One worktree per ticket under D:/OPC/goauto-worktrees/issue-<N>; runtime and release worktrees are excluded. After the branch is merged (and released if needed) the implementing agent removes it with git worktree remove after checking merged/clean/process/link state, never --force and never deleting branches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Document the local SynapBus service, per-tool identities (goauto, goauto_codex) and the #goauto channel, and the rule that agent messages are information, not authorization.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mirrors of Wiki revisions Architecture-and-Code-Map 4bf8482e,
Business-Rules-and-Glossary c0b1a842, SYB-ERP-Interface-Contract 5979a142,
Android-Agent-API-Contract 92e9f856: filter hits are stored and marked
(first creation only), PDD isolation, 无需采购 stage position, purchaseType
list filter, recompute preview/execute with fingerprint and 409, hit vs
markedCount wording. SYB-ERP page also restores #343's unified-session
paragraph (was only in the mirror) and corrects the structure rule to #286's
single `-#` all-characters rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
1. recomputeFingerprint now hashes a JSON-serialized (not naive string-
concatenated, to avoid delimiter-collision) sorted list of
{id, direction, ruleId, ruleKind, ruleKeyword} per planned change — the
rule kind/keyword are exactly what gets written into
excluded_rule_kind/excluded_rule_keyword, so a plan that affects the
same ids/directions via a since-edited rule must now be rejected as
stale, not silently accepted. New tests:
TestRecomputeFingerprintChangesWhenRuleEvidenceChanges (edits the rule
row directly between preview and execute, since the API has no edit
endpoint, and asserts RECOMPUTE_PREVIEW_STALE with nothing written and
no log row) and TestRecomputeFingerprintStableAcrossUnchangedPreviews
(two previews of the same data yield the same fingerprint and execute
succeeds).
2. New server/app/goauto/sybproductfilter/recompute_mysql_integration_test.go,
gated on GOAUTO_IT_MYSQL_DSN (t.Skip when unset, so `go test` is
unaffected normally). It creates a uniquely named throwaway database
(zz_goauto_it_340_<ts>), migrates it, and drops it in t.Cleanup — never
touches an existing database. Two real-MySQL, two-connection scenarios
reproduce the exact race the phase-3 fix closes: connection A takes its
REPEATABLE-READ snapshot via the planning step, connection B takes the
row's FOR UPDATE lock and holds it (confirmed via a channel) while A's
write phase is proven to actually block on that same lock (asserted via
a wait window), B then inserts a purchase_task / active return_match
and commits, and A is asserted to unblock, see it, and skip the row.
Verified locally against the dev MySQL server (this session never
printed the password: read via a shell one-liner into an env var,
exported only for the go test invocation): both tests PASS with the
phase-3 fix in place. Temporarily reverted purchaseTaskLockedQuery to a
plain (non-locking) read (not committed) and reran —
TestRecomputeConcurrentPurchaseTaskUnderRealMySQL correctly FAILED
("expected A to skip the row ... got {PDDToExcluded:1 SkippedHasTask:0}"),
proving the test is meaningful; restored and diffed byte-identical
against a backup before rerunning to confirm both tests pass again.
Confirmed via `SHOW DATABASES LIKE 'zz_goauto_it_%'` (empty) that every
throwaway database, across all these runs, was actually dropped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
writeRecomputeChanges' purchase_task/return_match rechecks (added in the
previous phase 3 commit) used plain SELECT COUNT(*). On MySQL 8.4 under
REPEATABLE-READ, a plain read inside a transaction reuses the snapshot
taken at that transaction's first read (recomputeChanges' own SELECT), so
a purchase_task or return_match row committed by another connection AFTER
that snapshot was invisible to the recheck even after the syb_product row's
FOR UPDATE lock was granted — the row lock only serializes writers against
each other, it doesn't force a later plain read to see newer committed
data. Confirmed against a real local MySQL 8.4 server with two connections:
after the other transaction committed a task, plain COUNT(*) returned 0
while COUNT(*) ... FOR SHARE correctly returned 1. Every existing test
passed anyway because this package's tests run on SQLite, which has no
multi-connection snapshot isolation to reproduce the race at all.
Fix: both rechecks now use clause.Locking{Strength: "SHARE"} (a locking/
"current" read is enough since they only need to observe committed rows,
not lock them further). Split the three per-row queries (row FOR UPDATE,
purchase_task FOR SHARE, return_match FOR SHARE) into small query-builder
helpers (sybProductRowLockQuery / purchaseTaskLockedQuery /
returnMatchLockedQuery) so writeRecomputeChanges consumes them and a test
can independently assert their generated SQL.
New test: TestRecheckQueriesUseLockingReads opens a DryRun gorm session
against the MySQL dialector (mysql.New with SkipInitializeWithVersion,
DisableAutomaticPing — no real network connection is ever made) and pins
the exact SQL shape via db.ToSQL:
SELECT * FROM `syb_product` WHERE id = 1 FOR UPDATE
SELECT count(*) FROM `purchase_task` WHERE syb_product_id = 1 FOR SHARE
SELECT count(*) FROM `return_match` WHERE syb_product_id = 1
AND active_syb_product_id IS NOT NULL FOR SHARE
The test's own comment documents why SQLite cannot reproduce this race
(gorm's SQLite driver drops clause.Locking entirely; SQLite also has no
multi-connection REPEATABLE-READ snapshot semantics to begin with).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F