fix: scope aidlc-learnings.ts's cid marker to the intent, not just the stage (2.5.67) - #747
Conversation
…e stage Fixes awslabs#735. cidMarker(slug, candidateId) keyed the write-idempotency marker on (stage slug, candidate id) only. Candidate ids restart at c1 on every stage run, and the destination files (project.md/team.md) are workspace-level, accumulating learnings from every intent that has ever run that stage. So an unrelated intent's own c1 for a previously-run stage collided with an earlier intent's marker already in the file. handlePersist's hasLine check then silently skipped the write while still emitting RULE_LEARNED and reporting rule_learned: 1, so audit and file state diverged with no error surfaced anywhere. cidMarker now takes the active intent's record-dir name (resolved via the existing activeIntent()) as a third key component. New markers cannot collide with old-format markers already written to a file, so this is forward-compatible with no migration. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
leandrodamascena
left a comment
There was a problem hiding this comment.
Thank you for identifying the cross-intent collision. The underlying bug is real, but the proposed marker does not yet provide a stable idempotency identity.
[P1] Candidate IDs still collide across stage attempts within one intent
Candidate IDs restart at c1 whenever candidates are surfaced. Adding the intent directory prevents A/c1 from colliding with B/c1, but a later execution of the same stage in A still reuses A/stage/c1. I reproduced two differently worded c1 selections in one intent: the second persist reported rule_learned: 0 and its text never reached project.md.
Location: core/tools/aidlc-learnings.ts:208-222,406-415,485-489.
[P1] Selections are not bound to their originating intent
Neither the surfaced output nor selections JSON carries space or intent identity. persist resolves whichever intent is active at execution time. I reproduced surfacing a selection under intent A, switching to B, then persisting it; the learning was written with B's marker. The intent is also resolved before the audit lock, while audit and destination paths resolve the live cursors again inside it.
Bind stable space/intent/candidate provenance into the surfaced selection and validate/pin it throughout the persist transaction.
Location: core/tools/aidlc-learnings.ts:225-232,268-271,428-442,461-478.
[P1] Existing markers are duplicated after upgrade
The new implementation searches only for the new marker format. On retrying a learning previously persisted as <!-- cid:<stage>:<candidate> -->, the old audit row is found but the old line marker is not. The tool appends the same learning again under the new marker. I reproduced one audit row with two practice lines after upgrade.
Add explicit legacy-marker compatibility or an in-place migration and pin exactly one audit row and one practice line.
Location: core/tools/aidlc-learnings.ts:413-415,485-499.
[P2] Ambiguous intent resolution must not fall back to unscoped
With multiple intents and no valid active cursor, activeIntent() returns null. Persist currently converts that ambiguity into the shared unscoped identity, recreating the original collision class. Fail closed unless this is an explicitly recognized legacy flat workspace.
Location: core/tools/aidlc-learnings.ts:430-434.
The focused suites pass (114/114) and package parity is clean, but they do not cover the scenarios above. After addressing them, the branch also needs a rebase onto current v2, a rebump to 2.5.70, and its new test renamed from t278 to t279 because #617 now owns t278.
Fixes #735
Summary
aidlc-learnings.ts persist's write-idempotency marker (cidMarker) is scoped to the active intent, not just the stage, so an unrelated intent's identically-numbered candidate can no longer silently overwrite an earlier intent's learning in the shared, workspace-levelproject.md/team.md.Changes
cidMarker(slug, candidateId)→cidMarker(intentSlug, slug, candidateId), producing<!-- cid:<intent-slug>:<stage-slug>:<candidate-id> -->instead of<!-- cid:<stage-slug>:<candidate-id> -->.handlePersistresolvesintentSlugonce via the already-exportedactiveIntent(projectDir)and threads it into the onecidMarkercall site. Falls back to the literal string"unscoped"when no active intent resolves (a project with noaidlc/spaces/.../intents/active-intentcursor) — this is a total, well-defined fallback, not an error path.dist/viabun scripts/package.ts(not hand-edited).t97,t158,t99) whose assertions pinned the old 2-part marker literally — the marker format legitimately changed, so these now assert the correct new value (unscoped:for fixtures with no active-intent cursor, the real seeded record-dir name for fixtures that have one).2.5.67(README badge + CHANGELOG).User experience
Before: Intent B runs a stage that Intent A also ran earlier in the same workspace, and Intent B's candidate happens to land on the same low candidate id (candidate ids restart at
c1on every stage run, so this is the common case, not an edge case).persistreportsrule_learned: 1and emits aRULE_LEARNEDaudit row, but Intent B's line never actually lands inproject.md/team.md— it silently no-ops against Intent A's already-present marker. Audit and file state diverge with no error anywhere; the only way to notice is an independent grep of the file against the reported count.After: Intent A's and Intent B's lines both land, each under its own distinct, intent-scoped marker. A same-intent, same-day re-run of the identical selection remains a correct no-op (crash-recovery idempotency preserved).
Upgrade: re-copy your
dist/<harness>/shell into the project.Checklist
Test Plan
tests/unit/t278-learnings-cid-intent-scope.test.ts: two intents' identically-numbered candidates for the same stage both persist side by side under distinct markers (confirmed RED against the pre-fixdist/, then GREEN after regenerating); a same-intent same-day re-run remains a correct no-op.bun test tests/unit/t97.test.ts tests/unit/t112-learnings-distribution-guard.test.ts tests/unit/t158-memory-writer-reader-seam.test.ts tests/unit/t199-learnings-memory-path.test.ts tests/unit/t227-tool-entrypoint-exports.test.ts tests/unit/t278-learnings-cid-intent-scope.test.ts tests/unit/t68-version-changelog-sync.test.ts tests/unit/gen-coverage-registry.test.ts tests/integration/t99-learnings-gate-flow.test.ts→ 114 pass, 0 fail.bun run check(package.ts --check+typecheck+lint) → clean (the only lint output is 3 pre-existing infos in an unrelated file,t267-usage.test.ts, unaffected by this branch).bash tests/run-tests.sh --ci --no-llm→ confirmed the 4 failing files outside the above set (t248-codekb-scope-diff,t248-copilot-packaging,t224-plugin-selection,t92) reproduce identically on a pristine, unmodifiedv2checkout (git-fingerprint-hash and tool-availability environment differences, unrelated to this change) — not a regression from this branch.Acknowledgment
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of the project license.