Issue #49: introduce CatalogWorkflowDependencies as the composition-only
adapter bundle and stop exposing implementation collaborators as workflow
properties. MainApp now assembles all production adapters explicitly (the
workflow creates no production defaults); tests assemble hermetic fakes via
a shared fixture in CatalogFixtures. Subscription tests swap fetch behavior
on the injected fake instead of mutating the workflow. An architecture
deletion check fails when UI source files reference implementation
collaborators or raw catalog/credential storage types, and a hermetic
assembly test proves the workflow runs entirely on injected fakes.
Issue #47: application-layer ProxyControlWorkflow wrapping the existing
ProxyRuntimeController with a whole-published ProxyControlSnapshot (typed
runtime facts, current mode, available modes, active-target facts with
safe path summary, skipped-invalid-server count, HTTP export capability)
and typed async commands (enable/disable/mode) that republish the full
snapshot on completion. Production runtime adapter delegates to the
controller without duplicating generation, health gating or system-proxy
ownership; catalog target facts arrive through a narrow seam
(CatalogTreeSnapshot.pathSummary) that never exposes the tree.
Status menu now reads runtime state and issues proxy commands only via
the workflow snapshot; catalog tree, activation and subscription actions
stay on CatalogWorkflow. MainApp wires the production adapters once as
composition root. No change to persistence order, failure taxonomy,
mode-availability policy (#46), catalog commit sync (#40), settings
(#44) or diagnostics (#43) seams.
Tests: fake-adapter snapshot/command/boundary contract tests plus
real-controller integration matrix (enable failures, approval, system
proxy, mode persistence failure and no-op) and an integration-level
status-menu model test proving the menu derives everything from the
workflow. Unavailable-mode no-op and runtime-stop-failure items of the
spec matrix are vacuous after ADR-0007 (no constructible unavailable
ProxyMode; controller stop path has no distinct failure category).
- rollback() returns CredentialRollbackOutcome and updateServer wraps
commit failures in CommitError carrying it, so tests assert through
the seam instead of poking credential store state
- hoist pluginNotManaged into form validation: form rejections stay
bare ServerFormError and never touch credentials
New UI must bind projections and send typed commands only.
Views were re-deriving delete confirm, move destinations,
activation eligibility, and drag payload from domain state.
Empty manual-group delete is no longer instant — always one confirmation.
Drop the UserDefaults intent mirror and read the toggle state only from the
system login-item registration status (approval-pending counts as enabled);
default to off until the user enables it, and stop resetting it with proxy
preferences (ADR-0005).
Add an application-layer CatalogCommitCoordinator as the single commit
entry for normal configuration changes: copy -> mutate -> atomic persist
-> publish committed state -> async runtime convergence toward the just
committed in-memory snapshot. Commits complete at persist; convergence
reports structured outcomes (converged / revalidated / failed /
clearedAndStopped) and never blocks or rolls back the commit. Each commit
bumps a generation; superseded syncs end before starting or have their
late results discarded, so only the latest snapshot can win. No automatic
retry.
CatalogRuntimeSyncing is the one new replacement seam: the production
ProxyRuntimeSyncAdapter drives the existing ProxyRuntimeController via a
snapshot-based catalogDidCommit; tests use a deterministic fake. When no
active target exists a commit no longer touches the runtime at all.
CatalogViewModel stays as a compatibility facade with unchanged published
state; the optional postCommit callback and the per-scene callback wiring
in MainApp are removed - the composition root wires the coordinator,
adapter and the (independent) Legacy import boundary exactly once.
Distinct id from Legacy (com.qiuyuzhou.ShadowsocksX-NG) lets NG2 own
~/Library/Application Support/ShadowsocksX-NG2/ directly; the v2/
subdir prefix is removed from all runtime paths.
- app/agent/tests targets + LaunchAgent plist renamed accordingly
- keychain service, queue labels, and Vendor signIdentifiers follow
the new bundle id; embedded helpers re-sign at build time
- Legacy handoff/import constants keep pointing at the old id
Migration: dev data under ShadowsocksX-NG/v2/ is not carried over
(fresh catalog/activation); old LaunchAgent registration and old
keychain entries must be cleaned up manually.
Status menu rebuilt around the D11 eight-item whitelist: header summary
(running status, mode, active target path), proxy toggle, mode picker with
checkmark, read-only active-target cascade over the catalog tree, update
all subscriptions, copy HTTP export line, open main window, and quit that
states the proxy keeps running in the background. Out-of-whitelist items
(copy PAC URL) removed. Two global shortcuts (KeyboardShortcuts 3.1.0)
toggle the proxy and cycle modes through the same controller entry points
as the menu.
Presentation logic extracted to StatusMenuModel (state summary mapping,
HTTP export line, mode cycle, read-only target tree) with unit tests;
activeTargetID is now published so the menu stays live when activation
happens while the proxy is off.
Ports as explicit user configuration (spec #21 D8): factory defaults
1086/1087/1089, named validation errors (range + pairwise-distinct),
free-port suggestion over 32768-65535 excluding other endpoints'
configured values, best-effort occupancy probe (bind test + lsof
process name), and persisted listen settings that refuse silent port
rewrites - an unreadable store stops the runtime with a named launch
failure instead of deploying placeholder ports. PAC-port changes
surface the shared-URL invalidation notice (UI lands with #33).
Runtime bind failure keeps flowing through the #29 health gate:
launch failed, system proxy untouched.
Shipped 1.x builds all carried CFBundleVersion 1: the old
Info.plist hardcoded it and the release flow only set the
marketing version. Same bundle id, so NG2 must exceed 1.
AGENTS.md keeps only the agent contract: hook enable step, gate
behavior, manual commands, rule-tuning policy. Install steps,
tool baselines, and config rationale are human-facing setup and
now live in README's 代码风格工具 section.
Update stale pointers that sent readers to AGENTS.md for tool
install (root AGENTS.md, pre-commit hook comment and error
message); hook behavior unchanged.
Pin sslocal v1.25.0 (aarch64-apple-darwin) and v2ray-plugin v1.3.2 via
committed manifests (tag + asset URL + archive SHA-256 + bundle location +
sign identifier):
- Scripts/fetch-external-binaries.sh downloads the pinned archive, verifies
its SHA-256 byte-for-byte, extracts only the named member (arm64-only
check) and stamps the local cache; archive hash mismatch fails hard, and
any manifest/local-artifact drift forces a re-verified refetch. It re-runs
as a pre-build phase so tampering fails the build itself.
- project.yml installs both binaries via Copy Files into
Contents/Helpers[/Plugins]; generate requires the artifacts to exist
(fetch before generate, else Copy Files phases are silently dropped).
- Scripts/sign-embedded-helpers.sh re-signs each helper Developer ID with
--options runtime --timestamp and the pinned identifier
(<bundle-id>[.plugin.]<name>) before Xcode seals the outer app signature.
- Scripts/packaging-gate.sh asserts strict nested signature verification,
DevID + Hardened Runtime on the app with no App Sandbox entitlement, and
exact per-binary manifest coverage under Contents/Helpers.
- Tests pin both manifests' schema and anchor hashes.
Verified: build + gate all green; byte-flip, foreign-binary and
missing-binary tampers each caught; 5 unit tests pass; no submodules or
in-repo builds; notarization stays out of scope (#39).
- ShadowsocksX-NG2/.swift-format: Apple Swift 6.4 toolchain default rules snapshot
- ShadowsocksX-NG2/.swiftlint.yml: default rules, excludes build artifacts and Legacy/
- .githooks/pre-commit: format staged NG2 Swift files into the index, then SwiftLint;
error-severity violations reject the commit; non-NG2 files pass through untouched
- ShadowsocksX-NG2/AGENTS.md: NG2-scoped conventions (tiered below root AGENTS.md)
- normalize skeleton sources to swift format baseline (2-space indent)