🐛 fix(standards): match rule removals and edits by content when the id is unknown - #527
Conversation
…d is unknown A client that reads standards from Markdown has no rule ids to send. The CLI stamps `deleteRule` and `updateRule` proposals with a placeholder and carries the rule's text instead — `payload.item.content` for a removal, `payload.oldValue` for an edit. The applier resolved both by id alone, so those proposals matched no rule and applied to nothing, while the batch still reported the standard as updated. Both branches now fall back to that text when the id names no rule. The id still wins whenever it resolves, so nothing changes for a client that sends a real one. The two fall back differently, on purpose: - A removal takes every rule with that content. The client is saying the standard should no longer carry it, and copies hold no id to tell them apart. - An edit takes the rule with that content only when exactly one has it. With several there is nothing to choose between them, and rewriting all would collapse them into duplicates, so it leaves them alone. Fixing this here rather than in the CLI means every client is repaired at once, including versions already installed that will never be upgraded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
…hing Review caught two problems with the content fallback. The match read the raw payload while applyDiff reads the effective one, so a reviewer who adjusted an accepted decision would have the rule chosen by the value the client first sent and the edit computed from the value they replaced it with — a different rule, or none. Both branches now resolve through getEffectivePayload. The fallback also left the version untouched when the content matched no rule, or several, and the apply still saved and reported success. That is the silent loss this change exists to remove, reached by a narrower route, so both branches now raise ChangeProposalConflictError instead: the signal applyDiff already uses for a change that cannot be applied cleanly, and one clients already surface. Only the fallback path raises. A proposal whose id resolves behaves exactly as before, so nothing that works today starts failing — an id-less proposal previously applied to nothing at all. The exception worth naming is re-applying an id-less proposal that already took effect, which now reports a conflict rather than passing silently. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 37726415 | Triggered | Basic Auth String | 589edc8 | apps/api/src/app/shared/middleware/GitRemoteCredentialsMiddleware.spec.ts | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secret safely. Learn here the best practices.
- Revoke and rotate this secret.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.
|
Hi @987poiuytrewq, Thanks a lot for the PR, it's merged! Don't hesitate to contribute again. |
Explanation
Follow-up to #515, which @quentinlebourles-packmind rightly declined: the fix belongs on the server, because a CLI fix only reaches whoever upgrades while every installed CLI keeps losing changes. This is that server-side fix. #515 can be closed once this lands.
A client that reads standards from Markdown has no rule ids to send — Markdown carries none. The CLI stamps
deleteRuleandupdateRuleproposals withcreateRuleId('unresolved')and identifies the rule by its text instead, which the payloads already carry:deleteRule→CollectionItemRemovePayload, sopayload.item.contentupdateRule→payload.oldValueStandardChangeProposalApplierresolved both by id alone:The placeholder matches no rule, so the filter removed nothing and
updateRule'smaprewrote nothing — while the batch still reported the standard as updated and the CLI cleared its staged change. Nothing was left locally to retry.Both branches now fall back to that text when the id names no rule. The id still wins whenever it resolves, so nothing changes for a client that sends a real one, and no protocol change is needed.
The two fall back differently, on purpose
When the fallback resolves to nothing usable — no rule carries the content, or several do and the change is an edit — it raises
ChangeProposalConflictErrorrather than saving an unchanged version and reporting success. That is the signalapplyDiffalready uses, and clients already surface it. Only the fallback path raises: a proposal whosetargetIdresolves behaves exactly as before.Type of Change
Affected Components
packages/types(playbookChangeManagement/applier/StandardChangeProposalApplier.ts) — reached bypackages/playbook-change-applierviaStandardChangesAppliertargetIdresolves; the fallback only runs where the change previously applied to nothing.Testing
Test Details:
Eleven tests added to
StandardChangeProposalApplier.spec.ts.updateRule, when the target id names no rule:ChangeProposalConflictErrorwhen no rule carries itupdateRule, when the target id names a rule:deleteRule, when the target id names no rule:ChangeProposalConflictErrorwhen no rule carries that contentdeleteRule, when the target id names a rule:Eight of the eleven fail against
mainwith only the spec applied. The other three are guards: they pass either way and exist to pin the id path, so the fallback cannot start firing where an id already resolves.nx run-many -t test,typecheck,lint --projects=types,playbook-change-applieris green (681 + 97 tests). The pre-push hook'snx affected -t lint test buildpassed across 21 projects.Verified by hand beforehand, against a live instance and the published
@packmind/cli@0.35.1: removing a rule with--no-reviewreported1 standard updatedand left the rule in place. That is the behaviour this repairs, and it repairs it for that already-published CLI without anyone upgrading.TODO List
Reviewer Notes
One behaviour change worth your judgement. Re-applying an id-less proposal that already took effect now reports a conflict rather than passing silently, because after the first apply neither the id nor the old content matches anything. If there is a retry path that re-applies accepted proposals, that is where it would show. Small blast radius, since only the CLI produces id-less proposals, but real.
targetIdis still matched against the raw payload. That predates this PR, so I have left it: if a decision can carry atargetIddiffering from the proposal's, the id would be resolved from one and the diff from the other. Happy to fold it in, but it changes how ids resolve, which felt like more than a fallback fix should decide.Two smaller notes:
updateRuleviamatchUpdatedRuleswhen the contents are similar, so a merely reworded rule takes the edit path and was affected in the same way.🤖 Generated with Claude Code