diff --git a/apps/labfox/lib/features/merge_requests/presentation/controllers/mr_discussions_controller.dart b/apps/labfox/lib/features/merge_requests/presentation/controllers/mr_discussions_controller.dart index 39eb37aa..5ad5fec3 100644 --- a/apps/labfox/lib/features/merge_requests/presentation/controllers/mr_discussions_controller.dart +++ b/apps/labfox/lib/features/merge_requests/presentation/controllers/mr_discussions_controller.dart @@ -11,11 +11,13 @@ import '../../../comments/data/comments_repository.dart'; import '../../../comments/data/discussion_resolution.dart'; import '../../../comments/data/suggestion_application.dart'; import '../../../comments/presentation/controllers/comments_controller.dart'; +import '../../../commits/data/history_repository.dart'; import '../../../diff/presentation/controllers/diff_controllers.dart'; import '../../data/merge_requests_repository.dart'; import '../../data/mr_draft_notes_repository.dart'; import 'merge_requests_controllers.dart'; import 'mr_draft_notes_provider.dart'; +import 'mr_original_review_controller.dart'; import 'mr_review_snapshot_controller.dart'; /// Reads grouped conversations and writes notes and replies in one session. @@ -870,6 +872,92 @@ class MrDiscussionsController ); } + /// Saves one original-commit selection after fresh context and parent reads. + Future savePendingCommitNote( + String note, { + required MrOriginalReviewRef original, + required DiffNotePosition position, + bool Function()? isCurrent, + }) { + if (note.trim().isEmpty || + pendingSaveNeedsInspection || + original.projectId != arg.projectId || + original.iid != arg.iid) { + return Future.value(null); + } + final provider = mrOriginalReviewControllerProvider(original); + final displayed = ref.read(provider); + if (displayed.isLoading || displayed.hasError || !displayed.hasValue) { + return Future.value(null); + } + final selection = MrOriginalReviewSelection.capture( + displayed.value!, + position, + ); + if (selection == null) return Future.value(null); + return _pendingReview( + (repository, detail, current, wait) async { + if (detail.id != original.mergeRequestId || + detail.targetProjectId != original.projectId) { + throw const GitLabConflictException( + 'The original MR identity changed.', + ); + } + final client = await wait(ref.read(gitLabClientProvider.future)); + if (!current() || client == null) throw const _PendingReviewCancelled(); + final parent = await wait( + HistoryRepository(client).originalCommit( + projectId: original.projectId, + commitId: position.baseSha!, + isCurrent: current, + ), + ); + if (!current()) throw const _PendingReviewCancelled(); + if (parent == null || parent.id != position.baseSha) { + throw const GitLabServerException('Unconfirmed original parent.'); + } + await wait(Future.value()); + if (!current()) throw const _PendingReviewCancelled(); + _pendingNeedsInspection = true; + final write = repository.createCommit( + projectId: arg.projectId, + iid: arg.iid, + mergeRequestId: detail.id, + commitId: original.commitId, + note: note, + position: position, + ); + _pendingWriteSettled = write.then( + (_) {}, + onError: (Object _, StackTrace _) {}, + ); + final saved = await wait(write); + if (!current()) throw const _PendingReviewCancelled(); + if (saved.id < 1 || + saved.authorId != repository.authorId || + saved.mergeRequestId != detail.id || + saved.note != note || + saved.commitId != original.commitId || + saved.resolveDiscussion != false || + saved.discussionId != null || + !_confirmedPosition(position, saved.position)) { + throw const GitLabServerException( + 'Unconfirmed private commit review save.', + ); + } + await wait(Future.value()); + if (!current()) throw const _PendingReviewCancelled(); + _pendingNeedsInspection = false; + _pendingWriteSettled = null; + ref.read(mrDraftNotesRevisionProvider(arg).notifier).state++; + return saved; + }, + isCurrent: isCurrent, + original: original, + originalSelection: selection, + ); + } + bool _confirmedPosition(DiffNotePosition? sent, DiffNotePosition? saved) { if (sent == null) { return saved == null || @@ -1063,6 +1151,8 @@ class MrDiscussionsController action, { bool Function()? isCurrent, MrReviewSelection? selection, + MrOriginalReviewRef? original, + MrOriginalReviewSelection? originalSelection, }) async { if (_posting || _loadingMore || @@ -1100,6 +1190,14 @@ class MrDiscussionsController Object? reviewSession = selection == null ? null : ref.read(reviewProvider.notifier).session; + final originalProvider = original == null + ? null + : mrOriginalReviewControllerProvider(original); + Future? originalFuture = + originalProvider == null ? null : ref.read(originalProvider.future); + Object? originalSession = originalProvider == null + ? null + : ref.read(originalProvider.notifier).session; Future? detailFuture; bool current() => generation == _generation && @@ -1127,6 +1225,13 @@ class MrDiscussionsController )) && (reviewFuture == null || identical(ref.read(reviewProvider.future), reviewFuture)) && + (originalSession == null || + identical( + ref.read(originalProvider!.notifier).session, + originalSession, + )) && + (originalFuture == null || + identical(ref.read(originalProvider!.future), originalFuture)) && (detailFuture == null || identical( ref.read(mergeRequestControllerProvider(arg).future), @@ -1156,6 +1261,23 @@ class MrDiscussionsController subscriptions.add(ref.listen(reviewProvider, (_, _) => check())); } + void observeOriginalEnd() { + if (originalProvider == null) return; + final sessionEnd = ref.read(originalProvider.notifier).ended; + // Reloading an already-loading provider may not emit a distinct state. + // Its explicit session end still releases unresolved preflight reads. + unawaited( + Future.any([sessionEnd, ended.future]).then((_) { + if (!ended.isCompleted) check(); + }), + ); + } + + if (originalProvider != null) { + subscriptions.add(ref.listen(originalProvider, (_, _) => check())); + observeOriginalEnd(); + } + // Bridge session cancellation once per command. Individual reads use the // local signal, which is completed in finally to release their handlers. unawaited( @@ -1224,6 +1346,30 @@ class MrDiscussionsController throw const GitLabConflictException('The selected diff changed.'); } } + if (originalProvider != null && originalSelection != null) { + originalFuture = null; + originalSession = null; + ref.invalidate(originalProvider); + final freshOriginal = ref.read(originalProvider.future); + originalFuture = freshOriginal; + originalSession = ref.read(originalProvider.notifier).session; + observeOriginalEnd(); + final snapshot = await wait(freshOriginal); + if (!current()) return null; + final origin = snapshot.context?.mergeRequest; + if (origin == null || + origin.id != detail.id || + origin.iid != detail.iid || + origin.targetProjectId != detail.targetProjectId || + origin.sourceProjectId != detail.sourceProjectId || + origin.sourceBranch != detail.sourceBranch || + origin.targetBranch != detail.targetBranch || + !originalSelection.matches(snapshot)) { + throw const GitLabConflictException( + 'The selected original diff changed.', + ); + } + } _syncPendingSession(); return await action(repository, detail, current, wait); } on _PendingReviewCancelled { diff --git a/apps/labfox/lib/features/merge_requests/presentation/controllers/mr_original_review_controller.dart b/apps/labfox/lib/features/merge_requests/presentation/controllers/mr_original_review_controller.dart index 88e83e8b..03991f7c 100644 --- a/apps/labfox/lib/features/merge_requests/presentation/controllers/mr_original_review_controller.dart +++ b/apps/labfox/lib/features/merge_requests/presentation/controllers/mr_original_review_controller.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gitlab_api/gitlab_api.dart'; import 'package:gitlab_models/gitlab_models.dart'; @@ -208,7 +209,11 @@ class MrOriginalReviewController int _generation = 0; Object _session = Object(); Object get session => _session; + Completer _ended = Completer(); + Future get ended => _ended.future; void _end() { + if (!_ended.isCompleted) _ended.complete(); + _ended = Completer(); _generation++; _session = Object(); } diff --git a/apps/labfox/test/mr_pending_original_review_save_test.dart b/apps/labfox/test/mr_pending_original_review_save_test.dart new file mode 100644 index 00000000..30d3a342 --- /dev/null +++ b/apps/labfox/test/mr_pending_original_review_save_test.dart @@ -0,0 +1,521 @@ +import 'dart:async'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:gitlab_api/gitlab_api.dart'; +import 'package:gitlab_models/gitlab_models.dart'; +import 'package:labfox/core/analytics/analytics.dart'; +import 'package:labfox/core/auth/auth_controller.dart'; +import 'package:labfox/core/auth/gitlab_client_provider.dart'; +import 'package:labfox/features/comments/presentation/controllers/comments_controller.dart'; +import 'package:labfox/features/merge_requests/data/merge_requests_repository.dart'; +import 'package:labfox/features/merge_requests/data/mr_draft_notes_repository.dart'; +import 'package:labfox/features/merge_requests/presentation/controllers/merge_requests_controllers.dart'; +import 'package:labfox/features/merge_requests/presentation/controllers/mr_discussions_controller.dart'; +import 'package:labfox/features/merge_requests/presentation/controllers/mr_draft_notes_provider.dart'; +import 'package:labfox/features/merge_requests/presentation/controllers/mr_original_review_controller.dart'; + +import '../../../packages/gitlab_api/test/mr_commits_api_test.dart' as api; +import 'mr_original_review_snapshot_test.dart' as original; +import 'mr_pending_review_save_test.dart' as pending; + +class Details extends pending.Details { + Details(super.client) { + result = original.mr; + } + Object contextResult = original.context(); + int contexts = 0; + @override + Future originalCommitContext({ + required int projectId, + required int iid, + required int mergeRequestId, + required String commitId, + required bool Function() isCurrent, + }) async { + contexts++; + final value = contextResult; + if (value is Future) return value; + if (value is MrOriginalCommitContext) return value; + throw value; + } +} + +class Fixture { + Fixture() { + client = api.client((o) { + parentReads++; + expect(o.method, 'GET'); + expect(o.path, '/projects/8/repository/commits/${original.parent}'); + if (parentResult is Future) { + return (parentResult as Future).then((v) => api.response(v)); + } + if (parentResult is GitLabException) return api.response({}, status: 404); + return api.response(parentResult); + }); + details = Details(client); + mockDrafts = CommitDrafts(client); + comments = pending.Comments(client); + c = ProviderContainer( + overrides: [ + analyticsProvider.overrideWithValue(events), + currentAccountProvider.overrideWith( + (ref) => ref.watch(pending.accountState), + ), + pending.clientState.overrideWith((ref) async => client), + gitLabClientProvider.overrideWith( + (ref) => ref.watch(pending.clientState), + ), + pending.sourceState.overrideWith((ref) async => details), + mergeRequestsRepositoryProvider.overrideWith( + (ref) => ref.watch(pending.sourceState), + ), + pending.draftState.overrideWith((ref) async => mockDrafts), + mrDraftNotesRepositoryProvider.overrideWith( + (ref) => ref.watch(pending.draftState), + ), + commentsRepositoryProvider.overrideWith((ref) async => comments), + ], + ); + subscription = c.listen( + mrOriginalReviewControllerProvider(original.arg), + (_, _) {}, + ); + addTearDown(() { + subscription.close(); + c.dispose(); + client.close(); + }); + } + late final GitLabClient client; + late final Details details; + late final CommitDrafts mockDrafts; + late final pending.Comments comments; + late final ProviderContainer c; + late final ProviderSubscription> + subscription; + final events = pending.Events(); + int parentReads = 0; + Object parentResult = api.commit(id: original.parent); + late DiffNotePosition position; + MrDiscussionsController get controller => + c.read(mrDiscussionsControllerProvider(pending.resource).notifier); + Future ready() async { + await c.read(mrDiscussionsControllerProvider(pending.resource).future); + final s = await c.read( + mrOriginalReviewControllerProvider(original.arg).future, + ); + final file = s.files.single; + position = s.positionFor(file, file.hunks.single.lines.last)!; + mockDrafts.position = position; + } + + Future save({ + bool Function()? current, + MrOriginalReviewRef? query, + String body = pending.markdown, + }) => controller.savePendingCommitNote( + body, + original: query ?? original.arg, + position: position, + isCurrent: current, + ); +} + +class CommitDrafts extends pending.Drafts { + CommitDrafts(super.client); + late DiffNotePosition position; + final commits = <(int, int, int, String, String, DiffNotePosition)>[]; + Object? outcome; + MergeRequestDraftNote get saved => pending + .draft(7) + .copyWith( + commitId: original.head, + position: position, + resolveDiscussion: false, + ); + @override + Future createCommit({ + required int projectId, + required int iid, + required int mergeRequestId, + required String commitId, + required String note, + required DiffNotePosition position, + }) async { + commits.add((projectId, iid, mergeRequestId, commitId, note, position)); + final v = outcome; + if (v is Future) return v; + if (v is MergeRequestDraftNote) return v; + if (v != null) throw v; + return saved; + } +} + +void main() { + test( + 'real captured repositories read target context and parent before one private POST', + () async { + final calls = []; + late DiffNotePosition position; + final client = api.client((o) { + calls.add('${o.method} ${o.path}'); + if (o.method == 'POST') { + expect(o.path, '/projects/8/merge_requests/142/draft_notes'); + expect(o.extra['labfox_no_auth_retry'], true); + expect(o.followRedirects, false); + final body = o.data as Map; + expect(body['note'], pending.markdown); + expect(body['commit_id'], original.head); + expect(body['resolve_discussion'], false); + expect( + body['position'], + position.toJson()..removeWhere((k, v) => v == null), + ); + return api.response({ + 'id': 7, + 'author_id': 23, + 'merge_request_id': 1100, + 'note': pending.markdown, + 'commit_id': original.head, + 'resolve_discussion': false, + 'position': position.toJson(), + }, status: 201); + } + if (o.path.endsWith('/discussions')) return api.response([]); + if (o.path == '/projects/8/merge_requests/142') { + return api.response(original.mr.toJson()); + } + if (o.path.endsWith('/merge_requests/142/commits')) { + return api.response([ + { + ...api.commit(), + 'parent_ids': [original.parent], + }, + ]); + } + if (o.path.endsWith('/diff')) { + return api.response([original.literal.toJson()]); + } + if (o.path.endsWith(original.parent)) { + return api.response(api.commit(id: original.parent)); + } + return api.response({ + ...api.commit(), + 'parent_ids': [original.parent], + }); + }); + addTearDown(client.close); + final c = ProviderContainer( + overrides: [ + currentAccountProvider.overrideWithValue(pending.account), + gitLabClientProvider.overrideWith((ref) async => client), + mergeRequestsRepositoryProvider.overrideWith( + (ref) async => MergeRequestsRepository(client), + ), + commentsRepositoryProvider.overrideWith( + (ref) async => pending.Comments(client), + ), + mrDraftNotesRepositoryProvider.overrideWith( + (ref) async => MrDraftNotesRepository(client, authorId: 23), + ), + ], + ); + addTearDown(c.dispose); + final sub = c.listen( + mrOriginalReviewControllerProvider(original.arg), + (_, _) {}, + ); + addTearDown(sub.close); + await c.read(mrDiscussionsControllerProvider(pending.resource).future); + final snapshot = await c.read( + mrOriginalReviewControllerProvider(original.arg).future, + ); + final file = snapshot.files.single; + position = snapshot.positionFor(file, file.hunks.single.lines.last)!; + calls.clear(); + final saved = await c + .read(mrDiscussionsControllerProvider(pending.resource).notifier) + .savePendingCommitNote( + pending.markdown, + original: original.arg, + position: position, + ); + expect(saved!.commitId, original.head); + expect(calls.length, 9); + expect( + calls.take(7).every((s) => s.startsWith('GET /projects/8/')), + true, + ); + expect(calls[7], 'GET /projects/8/repository/commits/${original.parent}'); + expect(calls.last, 'POST /projects/8/merge_requests/142/draft_notes'); + }, + ); + test( + 'multiline original selection preserves canonical range in one save', + () async { + final f = Fixture(); + await f.ready(); + final s = f.c + .read(mrOriginalReviewControllerProvider(original.arg)) + .value!; + final file = s.files.single; + f.position = s.rangePositionFor( + file, + file.hunks.single.lines.first, + file.hunks.single.lines.last, + )!; + f.mockDrafts.position = f.position; + expect(await f.save(), f.mockDrafts.saved); + expect(f.mockDrafts.commits.single.$6.lineRange, f.position.lineRange); + }, + ); + test( + 'fresh context typed failure never sets uncertain-write inspection', + () async { + final f = Fixture(); + await f.ready(); + f.details.contextResult = const GitLabForbiddenException('Forbidden'); + await expectLater(f.save(), throwsA(isA())); + expect(f.parentReads, 0); + expect(f.controller.pendingSaveNeedsInspection, false); + }, + ); + test('view already obsolete dispatches no reads', () async { + final f = Fixture(); + await f.ready(); + expect(await f.save(current: () => false), null); + expect(f.details.contexts, 1); + expect(f.parentReads, 0); + }); + test( + 'snapshot reload cancels unresolved fresh context and discards late failure', + () async { + final f = Fixture(); + await f.ready(); + final context = Completer(); + f.details.contextResult = context.future; + final save = f.save(); + await pumpEventQueue(); + f.c.invalidate(mrOriginalReviewControllerProvider(original.arg)); + expect(await save.timeout(const Duration(seconds: 2)), null); + context.completeError(const GitLabServerException('Late')); + await pumpEventQueue(); + expect(f.parentReads, 0); + expect(f.mockDrafts.commits, isEmpty); + }, + ); + for (final wrong in [ + original.mr.copyWith(id: 1101), + original.mr.copyWith(targetProjectId: 9), + original.mr.copyWith(sourceProjectId: 20), + original.mr.copyWith(sourceBranch: 'changed'), + original.mr.copyWith(targetBranch: 'changed'), + ]) { + test( + 'fresh private detail cannot substitute original identity $wrong', + () async { + final f = Fixture(); + await f.ready(); + f.details.result = wrong; + await expectLater(f.save(), throwsA(isA())); + expect(f.parentReads, 0); + expect(f.mockDrafts.commits, isEmpty); + }, + ); + } + + test( + 'fresh original selection and parent read precede one exact private write', + () async { + final f = Fixture(); + await f.ready(); + expect(await f.save(), f.mockDrafts.saved); + expect(f.details.contexts, 2); + expect(f.parentReads, 1); + expect(f.mockDrafts.commits, [ + (8, 142, 1100, original.head, pending.markdown, f.position), + ]); + expect(f.mockDrafts.writes, isEmpty); + expect(f.comments.posts, isEmpty); + expect(f.events.names, isEmpty); + expect(f.controller.pendingSaveNeedsInspection, false); + }, + ); + for (final body in ['', ' \n ']) { + test('blank original note never reads or writes', () async { + final f = Fixture(); + await f.ready(); + expect(await f.save(body: body), null); + expect(f.details.contexts, 1); + expect(f.parentReads, 0); + expect(f.mockDrafts.commits, isEmpty); + }); + } + for (final value in [ + original.context(parents: null), + original.context(parents: []), + original.context(parents: [original.parent, original.other]), + original.context(parents: [original.other]), + original.context( + files: [ + original.literal.copyWith( + diff: original.raw.replaceFirst('same', 'changed'), + ), + ], + ), + original.context(detail: original.mr.copyWith(sourceProjectId: 20)), + ]) { + test( + 'changed original context rejects before parent or write: $value', + () async { + final f = Fixture(); + await f.ready(); + f.details.contextResult = value; + await expectLater(f.save(), throwsA(isA())); + expect(f.parentReads, 0); + expect(f.mockDrafts.commits, isEmpty); + }, + ); + } + test('wrong MR route is rejected without reads', () async { + final f = Fixture(); + await f.ready(); + expect( + await f.save( + query: ( + projectId: 9, + iid: 142, + mergeRequestId: 1100, + commitId: original.head, + ), + ), + null, + ); + expect(f.details.contexts, 1); + expect(f.parentReads, 0); + }); + test( + 'parent unavailable propagates typed failure without write or recovery gate', + () async { + final f = Fixture(); + await f.ready(); + f.parentResult = const GitLabNotFoundException('Missing'); + await expectLater(f.save(), throwsA(isA())); + expect(f.mockDrafts.commits, isEmpty); + expect(f.controller.pendingSaveNeedsInspection, false); + }, + ); + test( + 'original reservation blocks public, private and pagination while parent resolves', + () async { + final f = Fixture(); + await f.ready(); + final parent = Completer(); + f.parentResult = parent.future; + final save = f.save(); + await pumpEventQueue(); + expect(f.parentReads, 1); + expect(await f.controller.savePendingNote(pending.markdown), null); + await f.controller.post('Public'); + await f.controller.loadMore(); + expect(f.comments.posts, isEmpty); + expect(f.comments.pages, [1]); + parent.complete(api.commit(id: original.parent)); + expect(await save, isNotNull); + }, + ); + for (final cancel in ['account', 'client', 'source', 'reload', 'view']) { + test( + '$cancel cancellation before dispatch releases unresolved save', + () async { + final f = Fixture(); + await f.ready(); + final parent = Completer(); + f.parentResult = parent.future; + var active = true; + final save = f.save(current: () => active); + await pumpEventQueue(); + switch (cancel) { + case 'account': + f.c.read(pending.accountState.notifier).state = null; + case 'client': + f.c.read(pending.clientState.notifier).state = Future.value(null); + case 'source': + f.c.read(pending.sourceState.notifier).state = Future.value(null); + case 'reload': + f.c.invalidate(mrOriginalReviewControllerProvider(original.arg)); + case 'view': + active = false; + parent.complete(api.commit(id: original.parent)); + } + expect(await save.timeout(const Duration(seconds: 2)), null); + expect(f.mockDrafts.commits, isEmpty); + if (!parent.isCompleted) { + parent.complete(api.commit(id: original.parent)); + } + await pumpEventQueue(); + expect(f.mockDrafts.commits, isEmpty); + }, + ); + } + for (final bad in [ + 'error', + 'commit', + 'position', + 'author', + 'global', + 'body', + 'public', + ]) { + test( + 'uncertain $bad requires explicit read-only inspection and no replay', + () async { + final f = Fixture(); + await f.ready(); + final d = f.mockDrafts.saved; + f.mockDrafts.outcome = switch (bad) { + 'error' => const GitLabServerException('Uncertain'), + 'commit' => d.copyWith(commitId: original.other), + 'position' => d.copyWith(position: f.position.copyWith(newLine: 10)), + 'author' => d.copyWith(authorId: 24), + 'global' => d.copyWith(mergeRequestId: 1101), + 'body' => d.copyWith(note: 'different'), + _ => d.copyWith(resolveDiscussion: true), + }; + await expectLater(f.save(), throwsA(isA())); + expect(f.controller.pendingSaveNeedsInspection, true); + expect(await f.save(), null); + f.mockDrafts.pages[1] = pending.page([d], next: 2); + f.mockDrafts.pages[2] = pending.page([]); + expect(await f.controller.inspectPendingNotes(), [d]); + expect(f.mockDrafts.reads, [(8, 142, 1, 20), (8, 142, 2, 20)]); + expect(f.mockDrafts.commits.length, 1); + expect(f.comments.posts, isEmpty); + expect(f.controller.pendingSaveNeedsInspection, false); + }, + ); + } + test( + 'late cancelled write must settle before complete read-only inspection', + () async { + final f = Fixture(); + await f.ready(); + final write = Completer(); + f.mockDrafts.outcome = write.future; + final save = f.save(); + await pumpEventQueue(); + expect(f.mockDrafts.commits.length, 1); + f.c.invalidate(mrOriginalReviewControllerProvider(original.arg)); + expect(await save, null); + expect(f.controller.pendingSaveNeedsInspection, true); + final inspect = f.controller.inspectPendingNotes(); + await pumpEventQueue(); + expect(f.mockDrafts.reads, isEmpty); + write.complete(f.mockDrafts.saved); + expect(await inspect, isEmpty); + expect(f.mockDrafts.reads, [(8, 142, 1, 20)]); + expect(f.mockDrafts.commits.length, 1); + }, + ); +} diff --git a/docs/mobile-web-parity.md b/docs/mobile-web-parity.md index 6c1f84e2..568d3600 100644 --- a/docs/mobile-web-parity.md +++ b/docs/mobile-web-parity.md @@ -57,7 +57,7 @@ issues and PRs. Keep it open until its full acceptance boundary is verified. | MW-04 | P1 | Member invitations, role/expiry changes, and removal for groups/projects. | Queued | Issue needed | | MW-05 | P0 | Issue/work-item editing, metadata, types, and validated state transitions. | In progress | [#329](https://github.com/Theorvane/labfox/issues/329), [PR #330](https://github.com/Theorvane/labfox/pull/330) shipped title and description editing; [#339](https://github.com/Theorvane/labfox/issues/339), [PR #340](https://github.com/Theorvane/labfox/pull/340) shipped due-date editing; [#341](https://github.com/Theorvane/labfox/issues/341), [PR #342](https://github.com/Theorvane/labfox/pull/342) shipped issue-label editing; [#343](https://github.com/Theorvane/labfox/issues/343), [PR #344](https://github.com/Theorvane/labfox/pull/344) shipped assignee editing; [#345](https://github.com/Theorvane/labfox/issues/345), [PR #346](https://github.com/Theorvane/labfox/pull/346) shipped milestone editing; [#347](https://github.com/Theorvane/labfox/issues/347), [PR #348](https://github.com/Theorvane/labfox/pull/348) shipped confidentiality; [#349](https://github.com/Theorvane/labfox/issues/349), [PR #350](https://github.com/Theorvane/labfox/pull/350) shipped discussion locking. Type-conversion compatibility and remaining work-item actions need separate issues. | | MW-06 | P1 | Boards and iterations: discover mobile-web behavior, then list/detail/mutations. | Queued | Issue needed | -| MW-07 | P0 | Advanced MR review: audit inline discussions/suggestions and finish missing review/approval flows. | In progress | [#552](https://github.com/Theorvane/labfox/issues/552), [PR #553](https://github.com/Theorvane/labfox/pull/553) prevent overlapping approval, merge, and other MR commands per project/IID, including while the repository resolves; failures release the reservation for explicit retry. [#554](https://github.com/Theorvane/labfox/issues/554), [PR #555](https://github.com/Theorvane/labfox/pull/555) add repository-session observation, pre-dispatch cancellation and late-result/error isolation, success-only current-session refresh/analytics/inbox effects, cancelled to-do outcomes without current-account toasts, and generation-owned duplicate reservations. Already dispatched server writes cannot be undone. [#556](https://github.com/Theorvane/labfox/issues/556), [PR #557](https://github.com/Theorvane/labfox/pull/557) add localized approval-read loading/error states, disabled approval toggles until successful reads, read-only retry, retained-data refresh protection, reactive new-session reads with stale-result isolation, and current-state toggle selection while preserving unavailable endpoint compatibility and independent merge controls. [#558](https://github.com/Theorvane/labfox/issues/558), [PR #559](https://github.com/Theorvane/labfox/pull/559) derives approval ownership from the authenticated account user ID and documented `approved_by` membership, ignores conflicting legacy current-user flags, and refreshes membership on account replacement. Regression coverage checks real toggle dispatch on mobile/desktop in both themes. [#560](https://github.com/Theorvane/labfox/issues/560), [PR #561](https://github.com/Theorvane/labfox/pull/561) validates successful approval responses before model parsing: explicit approver lists, complete user wrappers, positive integral user IDs, and nonnegative integral counts when present. Malformed reads become sanitized domain errors; status mapping precedes payload parsing. End-to-end widget tests verify blocked approval toggles and read-only recovery in both widths/themes, while unavailable endpoint compatibility remains intact. [#562](https://github.com/Theorvane/labfox/issues/562), [PR #563](https://github.com/Theorvane/labfox/pull/563) adds an account-bound, one-page MR discussion reader with generated grouped-note models and nullable resolution metadata. It preserves next-page headers without assuming totals or fetching ahead, rejects incomplete groups and nonadvancing cursors, and keeps HTTP failures distinct from sanitized parsing errors. Building on the API/models foundation, [#564](https://github.com/Theorvane/labfox/issues/564), [PR #565](https://github.com/Theorvane/labfox/pull/565) connects grouped MR replies to the detail-screen comments flow and aggregates resolution across all resolvable notes without claiming unknown states are resolved. The shared top-level composer refreshes discussion page one after posting; explicit load-more and read/page retries preserve groups and cursors. Account-bound reads/posts ignore late results, drafts reset when the session or resource changes, and resizing preserves the current draft. Five locales and narrow/compact/wide light/dark layouts are covered. [#566](https://github.com/Theorvane/labfox/issues/566), [PR #567](https://github.com/Theorvane/labfox/pull/567) adds the MR discussion reply API foundation: encoded thread/project paths, explicit IID routing, exact nonempty Markdown bodies, validated generated note responses, sanitized typed errors and no automatic authentication replay for the write. [#568](https://github.com/Theorvane/labfox/issues/568), [PR #569](https://github.com/Theorvane/labfox/pull/569) connects a localized inline reply composer to the selected discussion. One active reply preserves the top-level draft; cancel and write reservations prevent accidental overlaps. Replies preserve Markdown, retain failed drafts for explicit retry, refresh authoritative discussion page one and ignore old-session completions. Account/MR replacement resets the selection, resizing and pagination preserve current drafts, and a removed target restores top-level submission. Five locales and three widths in both themes are covered. [#570](https://github.com/Theorvane/labfox/issues/570), [PR #571](https://github.com/Theorvane/labfox/pull/571) adds known-state Resolve/Reopen controls and a strict account-bound mutation: encoded identity, explicit resolution, no redirects/authentication replay, and updated thread/state confirmation. Resolution shares comment/reply write reservations, preserves the top-level draft, retains localized permission/failure feedback for explicit retry, and refreshes discussions and MR detail only on current-session success. Unknown/nonresolvable states offer no toggle, and an active reply draft prevents resolution. [#572](https://github.com/Theorvane/labfox/issues/572), [PR #573](https://github.com/Theorvane/labfox/pull/573) preserves generated diff-note position metadata: original version SHAs, renamed paths, old/new text lines, multiline range endpoints and image/file types. Discussion reads, replies and resolution responses reject malformed or fractional coordinates before parsing; absent legacy fields remain unknown. [#574](https://github.com/Theorvane/labfox/issues/574), [PR #575](https://github.com/Theorvane/labfox/pull/575) adds paginated diff-version reads and an explicitly selected original unified-diff snapshot. Generated version/file models retain SHA triplets, renamed paths, raw text and nullable omission flags; absent files differ from empty snapshots. Identity/payload validation, status-first typed failures and no eager pagination or current-diff fallback are covered. [#576](https://github.com/Theorvane/labfox/issues/576) / [PR #577](https://github.com/Theorvane/labfox/pull/577) adds an expandable original-diff context beside positioned notes. It matches complete SHA triplets, both file paths and exact old/new single-line text coordinates; explicit older-page reads and read-only retries preserve drafts. The matched original hunk/line has an accessible non-color marker. Unsupported, omitted, truncated or ambiguous contexts remain unavailable without a current-diff fallback; account/resource replacement resets panels and stale reads cannot dispatch follow-up snapshots or affect the new session. Five locales, three widths and both themes are covered. Multiline context, positioned thread creation and suggestion application remain separate slices. [#578](https://github.com/Theorvane/labfox/issues/578), [PR #580](https://github.com/Theorvane/labfox/pull/580) adds the single-line positioned discussion creation API foundation: exact original SHA/path/line anchors, Markdown preservation, pre-dispatch argument validation, no redirects or authentication replay, created-thread confirmation and sanitized typed failures. The line selector/composer, multiline creation and suggestions remain separate. [#579](https://github.com/Theorvane/labfox/issues/579), [PR #581](https://github.com/Theorvane/labfox/pull/581) connects the latest authoritative diff-version snapshot to localized single-line selection and discussion creation. Exact original coordinates, indexed eligibility, shared write reservations, retained drafts, session/resource isolation and visible authoritative discussion inspection before uncertain-write retry are covered. The existing MR Changes route restores from project/IID; commit diffs stay read-only. Multiline context/creation and suggestion application remain separate. [#582](https://github.com/Theorvane/labfox/issues/582) / [PR #583](https://github.com/Theorvane/labfox/pull/583) adds typed immutable suggestion metadata to MR discussion responses, preserving original/replacement code, inclusive ranges, applied status and both documented applicability spellings. Missing states remain unknown; positive integral identities/coordinates, range order, response-wide uniqueness, content/flag types and alias agreement are validated before generated parsing. Suggestion presentation/application and multiline context/creation remain separate. [#584](https://github.com/Theorvane/labfox/issues/584) / [PR #585](https://github.com/Theorvane/labfox/pull/585) adds read-only suggestion previews beside user notes with exact selectable original/replacement code, nullable application/applicability states, localized ranges and empty/unavailable content, bounded code panes, retained drafts across resize/theme, and account/resource/metadata isolation. Suggestion application and batching remain separate. [#586](https://github.com/Theorvane/labfox/issues/586) / [PR #587](https://github.com/Theorvane/labfox/pull/587) adds the single-suggestion application API foundation with a positive global identity, exact optional commit messages, strict HTTP 200 and matching applied=true confirmation, shared metadata validation, typed sanitized failures, and no redirect/authentication replay. Application UI/session reservations and batch application remain separate. [#588](https://github.com/Theorvane/labfox/issues/588) / [PR #589](https://github.com/Theorvane/labfox/pull/589) adds explicit single-suggestion confirmation with literal code and optional commit messages, a fresh exact-discussion preflight, shared reservations including pagination, current-view/account isolation, confirmed-success detail/discussion/review-snapshot refreshes, and draft-preserving read-only recovery before uncertain-write retry. Five locales, three widths, both themes, keyboard insets and large text are covered. Batch application and multiline context/creation remain separate. [#590](https://github.com/Theorvane/labfox/issues/590) / [PR #591](https://github.com/Theorvane/labfox/pull/591) adds the batch suggestion API foundation: one account-bound request with immutable positive unique global IDs, exact optional messages, strict complete matching applied-state confirmation, preserved server order/nullable metadata, status-first sanitized failures, and no redirects, authentication replay or single-write fallback. Building on that API, [#592](https://github.com/Theorvane/labfox/issues/592) / [PR #593](https://github.com/Theorvane/labfox/pull/593) adds explicit loaded-target selection and multi-patch confirmation, deduplicated all-thread preflight before one batch, immutable selected identities, shared reservations, current-account/view isolation, complete staged read-only recovery, exact retained drafts and success-only refreshes. Five locales, three widths, both themes and compact keyboard/large-text layouts are covered; see [the batch UI](mr-suggestion-batch-ui.md). [#594](https://github.com/Theorvane/labfox/issues/594) / [PR #595](https://github.com/Theorvane/labfox/pull/595) adds exact original multiline context with inclusive side-aware per-line markers, full filename-hash/raw-counter endpoint identity, optional coordinate support, complete contiguous hunk checks, duplicate/reversed/gapped-span rejection and retained draft/session behavior. Five locales, three widths, both themes and compact keyboard/large-text layouts are covered; see [original context](mr-discussion-context.md). [#596](https://github.com/Theorvane/labfox/issues/596) / [PR #597](https://github.com/Theorvane/labfox/pull/597) adds explicit original multiline selection and positioned creation, indexed complete contiguous range eligibility, exact endpoint request/response confirmation, inclusive accessible markers, retained drafts and complete-range recovery inspection. Repository identity is checked before dispatch and after completion to isolate replaced-session outcomes. Five locales, three widths, both themes and compact keyboard/large-text layouts are covered; see [multiline creation](mr-multiline-discussions.md). [#598](https://github.com/Theorvane/labfox/issues/598) / [PR #599](https://github.com/Theorvane/labfox/pull/599) adds the read-only pending-review draft foundation: generated private note/global MR/author identities, exact Markdown and nullable original position/reply/resolution metadata; strict one-page draft_notes reads with immutable server order, header pagination, sanitized malformed/status failures and normal read-only OAuth refresh. Account-bound repositories reject other-author pages; auto-disposed page consumers prevent stale pre-dispatch reads and ignore late outcomes. Presentation removes retained private rows immediately during account/client/instance changes, sign-out, loading and failed refresh. Draft mutations, aggregation and pending-review UI remain separate; no live GitLab/device validation is claimed. [#600](https://github.com/Theorvane/labfox/issues/600) / [PR #601](https://github.com/Theorvane/labfox/pull/601) adds the unpublished regular/original text-range draft creation API prerequisite: exact Markdown and recursive null omission, strict HTTP 201 and original-position/body confirmation, no redirect/OAuth replay/automatic retry/public-comment fallback, and sanitized typed/status-preserving failures. Account-bound repositories require and confirm captured author plus explicit global MR identity separately from route IID. Shared text/range validation preserves published discussion behavior. Replies, commit/image/file drafts, editing/deletion, publication and the pending-review controller/UI remain follow-up work; uncertain writes require authoritative inspection before an explicit retry, and no live GitLab/device validation is claimed. [#602](https://github.com/Theorvane/labfox/issues/602) / [PR #603](https://github.com/Theorvane/labfox/pull/603) adds read-only pending-review pagination with an authoritative global MR identity separate from route IID, immutable server order, explicit cursors through empty pages, optional totals and duplicate-load prevention. Mismatched author/MR identities, duplicate drafts and backward cursors reject the full result. Account/repository generations cancel obsolete dispatch and isolate initial/pagination success/errors. Presentation immediately removes retained private rows during account/client/instance/sign-out, refresh and failed reads; explicit retry restarts page one. Offset traversal is not an atomic snapshot and cannot authorize publication. The controller exposes no UI or mutation; see [private pagination](mr-pending-review-reader.md). [#604](https://github.com/Theorvane/labfox/issues/604) / [PR #605](https://github.com/Theorvane/labfox/pull/605) adds a read-only private pending-review panel on MR detail with exact selectable saved Markdown, known original metadata, localized loading/empty/partial/error states and explicit paging/refresh/retry. Guarded queries await fresh MR detail and verify route/global identity before dispatch, closing account/instance/client replacement requests ahead of detail; old private rows disappear during detail refresh and failed reads. Five locales, three widths, both themes, large text, long content and resize/theme preservation are covered with six synthetic captures. The panel sends no mutation or embedded image request and never infers/re-anchors original positions; see [private review panel](mr-pending-review-panel.md). Creation orchestration, uncertain-write recovery and publication remain follow-ups; no live GitLab/device validation is claimed. [#606](https://github.com/Theorvane/labfox/issues/606) / [PR #607](https://github.com/Theorvane/LabFox/pull/607) adds regular private review save orchestration with fresh account-bound detail/global identity, shared discussion write/pagination reservations, exact Markdown and one confirmed create before route-scoped private-reader refresh. Uncertain writes block replay across ordinary refreshes and same-account client/repository replacement; explicit complete read-only inspection follows empty-page cursors, validates ownership/identity/uniqueness/order and waits for any dispatched write to settle. Old-session/view results and errors cannot expose private rows or dispatch further pages. The controller has no composer UI, positioned/reply save orchestration, editing/deletion or publication; see [guarded saves](mr-pending-review-save.md). Recovery is in-memory and offset inspection cannot promise exactly-once creation after a client timeout; no live-instance/device validation is claimed. [#608](https://github.com/Theorvane/LabFox/issues/608) / [PR #609](https://github.com/Theorvane/LabFox/pull/609) adds a localized regular private-note composer on MR detail with exact Markdown, guarded one-save orchestration, retained failed input, complete visible inspection and separate acknowledgement before manual retry. Account/client/repository/resource replacement discards private input and snapshots; obsolete results cannot toast or expose data. Public drafts stay intact, and same-account reopen retains uncertain-write gating. Five locales, three widths, both themes and keyboard/large-text recovery are covered with twelve synthetic captures; see [private composer](mr-pending-review-composer.md). Positioned/reply/commit saves, editing/deletion and publication remain separate; no exactly-once or live-instance/device validation is claimed. [#610](https://github.com/Theorvane/LabFox/issues/610) / [PR #611](https://github.com/Theorvane/LabFox/pull/611) adds private draft update/delete API and account-bound repository foundations: exact Markdown, explicit original text-position preservation, strict 200 target/body/position confirmation and 204 deletion acknowledgement, positive project/IID/draft identities, captured author/global-MR checks and unchanged non-body metadata. Status-first plain-wire decoding preserves typed failures even for malformed JSON; redirects, authentication replay, fallback writes and automatic retry are disabled. Original line-code-only and unsupported update anchors fail before dispatch; owned opaque/image/file drafts can be deleted by identity. See [maintenance contract](mr-draft-note-maintenance.md). Fresh target/controller reservations, localized edit/delete confirmation/recovery and publication remain follow-ups; no live-instance/device validation is claimed. [#612](https://github.com/Theorvane/LabFox/issues/612) connects private draft editing and confirmed deletion to the existing pending review panel. Shared discussion reservations, fresh detail/global identity and complete private-page comparison refuse stale or missing selected targets before one write. Uncertain results gate every private write; complete visible inspection and acknowledgement adopt a current target only for explicit retry, with obsolete session/view input and outcomes discarded. Five locales, keyboard/large text and twelve synthetic captures cover three widths and both themes; see [maintenance UI](mr-pending-review-maintenance.md). Publication and positioned/reply/commit creation remain separate; no atomic snapshot, conditional-write, durable recovery or live-instance/device guarantee is claimed. [#614](https://github.com/Theorvane/LabFox/issues/614) adds whole-review publication: complete visible private-note confirmation and consent, a fresh full-set comparison before one strict-204 account-scoped bulk POST, and uncertainty that blocks private mutations until complete private/public inspection. Manual retries require new consent; session/view changes discard old confirmation and outcomes. Five locales and three widths in both themes are covered; see [publication flow](mr-pending-review-publication.md). Single-note publication, summary/reviewer-state controls and positioned/reply/commit creation remain separate. No automatic approval/merge, atomic server snapshot, durable recovery or live-instance/device guarantee is claimed. [#616](https://github.com/Theorvane/labfox/issues/616) adds private text line/range draft creation from the MR changes screen, fresh authoritative diff selection checks, immutable selected text, session/snapshot/origin isolation, preserved public input and explicit uncertain-save inspection. See [private inline review](mr-pending-review-inline.md). [#618](https://github.com/Theorvane/LabFox/issues/618) adds selected saved-note publication through one strict-204 PUT, fresh exact target comparison, explicit consent and shared two-sided recovery. Unrelated saved notes remain private; a missing selected ID cannot publish another note. See [single-note publication](mr-pending-review-single-publication.md). [#620](https://github.com/Theorvane/LabFox/issues/620) adds whole-review submission with an exact public summary, explicit reviewed/request-changes outcomes, complete current-reviewer reads, fresh reviewer preflight comparison and postwrite outcome verification. Partial or obsolete outcomes require private/public/reviewer inspection after actual write settlement and renewed consent, including selected-note recovery entry. Five locales, three widths, both themes and keyboard/large text are covered; see [review submission](mr-review-submission.md). Formal approval remains separate; additional draft creation types are follow-ups. No atomic, durable recovery or live-instance/device guarantee is claimed. [#622](https://github.com/Theorvane/LabFox/issues/622) adds private reply drafts on existing public discussion groups, exact target/body and non-resolution confirmation, a fresh selected-thread preflight, shared command reservations, and actual-settlement recovery that stages every private page together with the original public target. Missing targets stay missing; scoped recovery cannot clear publication/reviewer uncertainty. The localized shared composer preserves unsent public comments/replies and survives its own detail refresh or target removal. Five locales, keyboard/large text and twelve synthetic captures cover three widths and both themes; see [private replies](mr-private-review-replies.md). Commit/image/file draft creation remains separate; no atomic, durable recovery or live-instance/device guarantee is claimed. [#624](https://github.com/Theorvane/LabFox/issues/624) adds the private commit-text draft API/repository foundation: exact commit/body/original text position, matching head SHA, strict private acknowledgement and captured author/global MR validation, with no redirect/auth replay/retry. No user action is exposed; guarded commit selection, membership/freshness, shared reservations and recovery remain follow-ups. See [commit-text draft API](mr-commit-text-draft-api.md). Image/file creation and MW-07 remain in progress. [#626](https://github.com/Theorvane/LabFox/issues/626) adds MR-scoped commit reads and complete advertised-page traversal, preserving unknown/root/ordered parent metadata. Strict SHA-1 identities, offset and same-resource Link cursor checks, cross-page duplicate rejection, immutable results and origin currency guards prevent partial or obsolete membership exposure. Read-only OAuth refresh remains account-bound. See [MR commit membership reads](mr-commit-membership-reads.md). Original commit diff selection and guarded private-save/recovery UI remain follow-ups; no atomic snapshot or live-instance guarantee is claimed. [#628](https://github.com/Theorvane/LabFox/issues/628) adds fixed-SHA literal commit diff pages and guarded advertised-page traversal, retaining raw paths/text and unknown omission flags. Shared offset validation preserves MR membership and confirms unified format plus exact route echoes. Diff file limits can stop pagination before full coverage; no original coordinate, membership or private-write preflight is inferred. See [original commit diff reads](original-commit-diff-reads.md). Authoritative fork/parent context and guarded literal selection/save/recovery UI remain follow-ups. [#630](https://github.com/Theorvane/LabFox/issues/630) preserves separate nullable fork project identities and adds a fresh captured MR detail context read. Strict integer and route/global identity checks reject malformed or mismatched responses; unknown target context is never inferred, while unknown source metadata stays unknown. Currency guards discard obsolete results and typed failures; read-only authentication remains captured. See [MR commit review context](mr-commit-review-context.md). Original parent/reference validation and guarded literal selection/save/recovery UI remain follow-ups; no atomic snapshot, original-coordinate or write-eligibility guarantee is claimed. [#632](https://github.com/Theorvane/LabFox/issues/632) adds strict fixed-head commit detail and guarded captured-account reads. Exact SHA and ordered nullable parent checks preserve unknown/root/merge metadata without synthesizing references; status-first failures, disabled redirects and obsolete-result isolation retain honest context. General branch/tag reads remain compatible. See [fixed commit review reads](fixed-commit-review-reads.md). Original reference/coordinate validation and guarded private-save/recovery UI remain follow-ups; no membership, atomic snapshot or write-eligibility guarantee is inferred. Captured original commit aggregation follows in #634 ([contract](mr-original-commit-context.md)); guarded literal coordinates and private-save UI remain in progress. Strict literal original diff validation and endpoint matching follow in #636 ([contract](literal-original-diff.md)); original refs and guarded private-save UI remain in progress. Captured single-parent original review snapshots and full-file selection fingerprints follow in #638 ([contract](original-review-snapshots.md)); guarded private-save UI remains in progress. | +| MW-07 | P0 | Advanced MR review: audit inline discussions/suggestions and finish missing review/approval flows. | In progress | [#552](https://github.com/Theorvane/labfox/issues/552), [PR #553](https://github.com/Theorvane/labfox/pull/553) prevent overlapping approval, merge, and other MR commands per project/IID, including while the repository resolves; failures release the reservation for explicit retry. [#554](https://github.com/Theorvane/labfox/issues/554), [PR #555](https://github.com/Theorvane/labfox/pull/555) add repository-session observation, pre-dispatch cancellation and late-result/error isolation, success-only current-session refresh/analytics/inbox effects, cancelled to-do outcomes without current-account toasts, and generation-owned duplicate reservations. Already dispatched server writes cannot be undone. [#556](https://github.com/Theorvane/labfox/issues/556), [PR #557](https://github.com/Theorvane/labfox/pull/557) add localized approval-read loading/error states, disabled approval toggles until successful reads, read-only retry, retained-data refresh protection, reactive new-session reads with stale-result isolation, and current-state toggle selection while preserving unavailable endpoint compatibility and independent merge controls. [#558](https://github.com/Theorvane/labfox/issues/558), [PR #559](https://github.com/Theorvane/labfox/pull/559) derives approval ownership from the authenticated account user ID and documented `approved_by` membership, ignores conflicting legacy current-user flags, and refreshes membership on account replacement. Regression coverage checks real toggle dispatch on mobile/desktop in both themes. [#560](https://github.com/Theorvane/labfox/issues/560), [PR #561](https://github.com/Theorvane/labfox/pull/561) validates successful approval responses before model parsing: explicit approver lists, complete user wrappers, positive integral user IDs, and nonnegative integral counts when present. Malformed reads become sanitized domain errors; status mapping precedes payload parsing. End-to-end widget tests verify blocked approval toggles and read-only recovery in both widths/themes, while unavailable endpoint compatibility remains intact. [#562](https://github.com/Theorvane/labfox/issues/562), [PR #563](https://github.com/Theorvane/labfox/pull/563) adds an account-bound, one-page MR discussion reader with generated grouped-note models and nullable resolution metadata. It preserves next-page headers without assuming totals or fetching ahead, rejects incomplete groups and nonadvancing cursors, and keeps HTTP failures distinct from sanitized parsing errors. Building on the API/models foundation, [#564](https://github.com/Theorvane/labfox/issues/564), [PR #565](https://github.com/Theorvane/labfox/pull/565) connects grouped MR replies to the detail-screen comments flow and aggregates resolution across all resolvable notes without claiming unknown states are resolved. The shared top-level composer refreshes discussion page one after posting; explicit load-more and read/page retries preserve groups and cursors. Account-bound reads/posts ignore late results, drafts reset when the session or resource changes, and resizing preserves the current draft. Five locales and narrow/compact/wide light/dark layouts are covered. [#566](https://github.com/Theorvane/labfox/issues/566), [PR #567](https://github.com/Theorvane/labfox/pull/567) adds the MR discussion reply API foundation: encoded thread/project paths, explicit IID routing, exact nonempty Markdown bodies, validated generated note responses, sanitized typed errors and no automatic authentication replay for the write. [#568](https://github.com/Theorvane/labfox/issues/568), [PR #569](https://github.com/Theorvane/labfox/pull/569) connects a localized inline reply composer to the selected discussion. One active reply preserves the top-level draft; cancel and write reservations prevent accidental overlaps. Replies preserve Markdown, retain failed drafts for explicit retry, refresh authoritative discussion page one and ignore old-session completions. Account/MR replacement resets the selection, resizing and pagination preserve current drafts, and a removed target restores top-level submission. Five locales and three widths in both themes are covered. [#570](https://github.com/Theorvane/labfox/issues/570), [PR #571](https://github.com/Theorvane/labfox/pull/571) adds known-state Resolve/Reopen controls and a strict account-bound mutation: encoded identity, explicit resolution, no redirects/authentication replay, and updated thread/state confirmation. Resolution shares comment/reply write reservations, preserves the top-level draft, retains localized permission/failure feedback for explicit retry, and refreshes discussions and MR detail only on current-session success. Unknown/nonresolvable states offer no toggle, and an active reply draft prevents resolution. [#572](https://github.com/Theorvane/labfox/issues/572), [PR #573](https://github.com/Theorvane/labfox/pull/573) preserves generated diff-note position metadata: original version SHAs, renamed paths, old/new text lines, multiline range endpoints and image/file types. Discussion reads, replies and resolution responses reject malformed or fractional coordinates before parsing; absent legacy fields remain unknown. [#574](https://github.com/Theorvane/labfox/issues/574), [PR #575](https://github.com/Theorvane/labfox/pull/575) adds paginated diff-version reads and an explicitly selected original unified-diff snapshot. Generated version/file models retain SHA triplets, renamed paths, raw text and nullable omission flags; absent files differ from empty snapshots. Identity/payload validation, status-first typed failures and no eager pagination or current-diff fallback are covered. [#576](https://github.com/Theorvane/labfox/issues/576) / [PR #577](https://github.com/Theorvane/labfox/pull/577) adds an expandable original-diff context beside positioned notes. It matches complete SHA triplets, both file paths and exact old/new single-line text coordinates; explicit older-page reads and read-only retries preserve drafts. The matched original hunk/line has an accessible non-color marker. Unsupported, omitted, truncated or ambiguous contexts remain unavailable without a current-diff fallback; account/resource replacement resets panels and stale reads cannot dispatch follow-up snapshots or affect the new session. Five locales, three widths and both themes are covered. Multiline context, positioned thread creation and suggestion application remain separate slices. [#578](https://github.com/Theorvane/labfox/issues/578), [PR #580](https://github.com/Theorvane/labfox/pull/580) adds the single-line positioned discussion creation API foundation: exact original SHA/path/line anchors, Markdown preservation, pre-dispatch argument validation, no redirects or authentication replay, created-thread confirmation and sanitized typed failures. The line selector/composer, multiline creation and suggestions remain separate. [#579](https://github.com/Theorvane/labfox/issues/579), [PR #581](https://github.com/Theorvane/labfox/pull/581) connects the latest authoritative diff-version snapshot to localized single-line selection and discussion creation. Exact original coordinates, indexed eligibility, shared write reservations, retained drafts, session/resource isolation and visible authoritative discussion inspection before uncertain-write retry are covered. The existing MR Changes route restores from project/IID; commit diffs stay read-only. Multiline context/creation and suggestion application remain separate. [#582](https://github.com/Theorvane/labfox/issues/582) / [PR #583](https://github.com/Theorvane/labfox/pull/583) adds typed immutable suggestion metadata to MR discussion responses, preserving original/replacement code, inclusive ranges, applied status and both documented applicability spellings. Missing states remain unknown; positive integral identities/coordinates, range order, response-wide uniqueness, content/flag types and alias agreement are validated before generated parsing. Suggestion presentation/application and multiline context/creation remain separate. [#584](https://github.com/Theorvane/labfox/issues/584) / [PR #585](https://github.com/Theorvane/labfox/pull/585) adds read-only suggestion previews beside user notes with exact selectable original/replacement code, nullable application/applicability states, localized ranges and empty/unavailable content, bounded code panes, retained drafts across resize/theme, and account/resource/metadata isolation. Suggestion application and batching remain separate. [#586](https://github.com/Theorvane/labfox/issues/586) / [PR #587](https://github.com/Theorvane/labfox/pull/587) adds the single-suggestion application API foundation with a positive global identity, exact optional commit messages, strict HTTP 200 and matching applied=true confirmation, shared metadata validation, typed sanitized failures, and no redirect/authentication replay. Application UI/session reservations and batch application remain separate. [#588](https://github.com/Theorvane/labfox/issues/588) / [PR #589](https://github.com/Theorvane/labfox/pull/589) adds explicit single-suggestion confirmation with literal code and optional commit messages, a fresh exact-discussion preflight, shared reservations including pagination, current-view/account isolation, confirmed-success detail/discussion/review-snapshot refreshes, and draft-preserving read-only recovery before uncertain-write retry. Five locales, three widths, both themes, keyboard insets and large text are covered. Batch application and multiline context/creation remain separate. [#590](https://github.com/Theorvane/labfox/issues/590) / [PR #591](https://github.com/Theorvane/labfox/pull/591) adds the batch suggestion API foundation: one account-bound request with immutable positive unique global IDs, exact optional messages, strict complete matching applied-state confirmation, preserved server order/nullable metadata, status-first sanitized failures, and no redirects, authentication replay or single-write fallback. Building on that API, [#592](https://github.com/Theorvane/labfox/issues/592) / [PR #593](https://github.com/Theorvane/labfox/pull/593) adds explicit loaded-target selection and multi-patch confirmation, deduplicated all-thread preflight before one batch, immutable selected identities, shared reservations, current-account/view isolation, complete staged read-only recovery, exact retained drafts and success-only refreshes. Five locales, three widths, both themes and compact keyboard/large-text layouts are covered; see [the batch UI](mr-suggestion-batch-ui.md). [#594](https://github.com/Theorvane/labfox/issues/594) / [PR #595](https://github.com/Theorvane/labfox/pull/595) adds exact original multiline context with inclusive side-aware per-line markers, full filename-hash/raw-counter endpoint identity, optional coordinate support, complete contiguous hunk checks, duplicate/reversed/gapped-span rejection and retained draft/session behavior. Five locales, three widths, both themes and compact keyboard/large-text layouts are covered; see [original context](mr-discussion-context.md). [#596](https://github.com/Theorvane/labfox/issues/596) / [PR #597](https://github.com/Theorvane/labfox/pull/597) adds explicit original multiline selection and positioned creation, indexed complete contiguous range eligibility, exact endpoint request/response confirmation, inclusive accessible markers, retained drafts and complete-range recovery inspection. Repository identity is checked before dispatch and after completion to isolate replaced-session outcomes. Five locales, three widths, both themes and compact keyboard/large-text layouts are covered; see [multiline creation](mr-multiline-discussions.md). [#598](https://github.com/Theorvane/labfox/issues/598) / [PR #599](https://github.com/Theorvane/labfox/pull/599) adds the read-only pending-review draft foundation: generated private note/global MR/author identities, exact Markdown and nullable original position/reply/resolution metadata; strict one-page draft_notes reads with immutable server order, header pagination, sanitized malformed/status failures and normal read-only OAuth refresh. Account-bound repositories reject other-author pages; auto-disposed page consumers prevent stale pre-dispatch reads and ignore late outcomes. Presentation removes retained private rows immediately during account/client/instance changes, sign-out, loading and failed refresh. Draft mutations, aggregation and pending-review UI remain separate; no live GitLab/device validation is claimed. [#600](https://github.com/Theorvane/labfox/issues/600) / [PR #601](https://github.com/Theorvane/labfox/pull/601) adds the unpublished regular/original text-range draft creation API prerequisite: exact Markdown and recursive null omission, strict HTTP 201 and original-position/body confirmation, no redirect/OAuth replay/automatic retry/public-comment fallback, and sanitized typed/status-preserving failures. Account-bound repositories require and confirm captured author plus explicit global MR identity separately from route IID. Shared text/range validation preserves published discussion behavior. Replies, commit/image/file drafts, editing/deletion, publication and the pending-review controller/UI remain follow-up work; uncertain writes require authoritative inspection before an explicit retry, and no live GitLab/device validation is claimed. [#602](https://github.com/Theorvane/labfox/issues/602) / [PR #603](https://github.com/Theorvane/labfox/pull/603) adds read-only pending-review pagination with an authoritative global MR identity separate from route IID, immutable server order, explicit cursors through empty pages, optional totals and duplicate-load prevention. Mismatched author/MR identities, duplicate drafts and backward cursors reject the full result. Account/repository generations cancel obsolete dispatch and isolate initial/pagination success/errors. Presentation immediately removes retained private rows during account/client/instance/sign-out, refresh and failed reads; explicit retry restarts page one. Offset traversal is not an atomic snapshot and cannot authorize publication. The controller exposes no UI or mutation; see [private pagination](mr-pending-review-reader.md). [#604](https://github.com/Theorvane/labfox/issues/604) / [PR #605](https://github.com/Theorvane/labfox/pull/605) adds a read-only private pending-review panel on MR detail with exact selectable saved Markdown, known original metadata, localized loading/empty/partial/error states and explicit paging/refresh/retry. Guarded queries await fresh MR detail and verify route/global identity before dispatch, closing account/instance/client replacement requests ahead of detail; old private rows disappear during detail refresh and failed reads. Five locales, three widths, both themes, large text, long content and resize/theme preservation are covered with six synthetic captures. The panel sends no mutation or embedded image request and never infers/re-anchors original positions; see [private review panel](mr-pending-review-panel.md). Creation orchestration, uncertain-write recovery and publication remain follow-ups; no live GitLab/device validation is claimed. [#606](https://github.com/Theorvane/labfox/issues/606) / [PR #607](https://github.com/Theorvane/LabFox/pull/607) adds regular private review save orchestration with fresh account-bound detail/global identity, shared discussion write/pagination reservations, exact Markdown and one confirmed create before route-scoped private-reader refresh. Uncertain writes block replay across ordinary refreshes and same-account client/repository replacement; explicit complete read-only inspection follows empty-page cursors, validates ownership/identity/uniqueness/order and waits for any dispatched write to settle. Old-session/view results and errors cannot expose private rows or dispatch further pages. The controller has no composer UI, positioned/reply save orchestration, editing/deletion or publication; see [guarded saves](mr-pending-review-save.md). Recovery is in-memory and offset inspection cannot promise exactly-once creation after a client timeout; no live-instance/device validation is claimed. [#608](https://github.com/Theorvane/LabFox/issues/608) / [PR #609](https://github.com/Theorvane/LabFox/pull/609) adds a localized regular private-note composer on MR detail with exact Markdown, guarded one-save orchestration, retained failed input, complete visible inspection and separate acknowledgement before manual retry. Account/client/repository/resource replacement discards private input and snapshots; obsolete results cannot toast or expose data. Public drafts stay intact, and same-account reopen retains uncertain-write gating. Five locales, three widths, both themes and keyboard/large-text recovery are covered with twelve synthetic captures; see [private composer](mr-pending-review-composer.md). Positioned/reply/commit saves, editing/deletion and publication remain separate; no exactly-once or live-instance/device validation is claimed. [#610](https://github.com/Theorvane/LabFox/issues/610) / [PR #611](https://github.com/Theorvane/LabFox/pull/611) adds private draft update/delete API and account-bound repository foundations: exact Markdown, explicit original text-position preservation, strict 200 target/body/position confirmation and 204 deletion acknowledgement, positive project/IID/draft identities, captured author/global-MR checks and unchanged non-body metadata. Status-first plain-wire decoding preserves typed failures even for malformed JSON; redirects, authentication replay, fallback writes and automatic retry are disabled. Original line-code-only and unsupported update anchors fail before dispatch; owned opaque/image/file drafts can be deleted by identity. See [maintenance contract](mr-draft-note-maintenance.md). Fresh target/controller reservations, localized edit/delete confirmation/recovery and publication remain follow-ups; no live-instance/device validation is claimed. [#612](https://github.com/Theorvane/LabFox/issues/612) connects private draft editing and confirmed deletion to the existing pending review panel. Shared discussion reservations, fresh detail/global identity and complete private-page comparison refuse stale or missing selected targets before one write. Uncertain results gate every private write; complete visible inspection and acknowledgement adopt a current target only for explicit retry, with obsolete session/view input and outcomes discarded. Five locales, keyboard/large text and twelve synthetic captures cover three widths and both themes; see [maintenance UI](mr-pending-review-maintenance.md). Publication and positioned/reply/commit creation remain separate; no atomic snapshot, conditional-write, durable recovery or live-instance/device guarantee is claimed. [#614](https://github.com/Theorvane/LabFox/issues/614) adds whole-review publication: complete visible private-note confirmation and consent, a fresh full-set comparison before one strict-204 account-scoped bulk POST, and uncertainty that blocks private mutations until complete private/public inspection. Manual retries require new consent; session/view changes discard old confirmation and outcomes. Five locales and three widths in both themes are covered; see [publication flow](mr-pending-review-publication.md). Single-note publication, summary/reviewer-state controls and positioned/reply/commit creation remain separate. No automatic approval/merge, atomic server snapshot, durable recovery or live-instance/device guarantee is claimed. [#616](https://github.com/Theorvane/labfox/issues/616) adds private text line/range draft creation from the MR changes screen, fresh authoritative diff selection checks, immutable selected text, session/snapshot/origin isolation, preserved public input and explicit uncertain-save inspection. See [private inline review](mr-pending-review-inline.md). [#618](https://github.com/Theorvane/LabFox/issues/618) adds selected saved-note publication through one strict-204 PUT, fresh exact target comparison, explicit consent and shared two-sided recovery. Unrelated saved notes remain private; a missing selected ID cannot publish another note. See [single-note publication](mr-pending-review-single-publication.md). [#620](https://github.com/Theorvane/LabFox/issues/620) adds whole-review submission with an exact public summary, explicit reviewed/request-changes outcomes, complete current-reviewer reads, fresh reviewer preflight comparison and postwrite outcome verification. Partial or obsolete outcomes require private/public/reviewer inspection after actual write settlement and renewed consent, including selected-note recovery entry. Five locales, three widths, both themes and keyboard/large text are covered; see [review submission](mr-review-submission.md). Formal approval remains separate; additional draft creation types are follow-ups. No atomic, durable recovery or live-instance/device guarantee is claimed. [#622](https://github.com/Theorvane/LabFox/issues/622) adds private reply drafts on existing public discussion groups, exact target/body and non-resolution confirmation, a fresh selected-thread preflight, shared command reservations, and actual-settlement recovery that stages every private page together with the original public target. Missing targets stay missing; scoped recovery cannot clear publication/reviewer uncertainty. The localized shared composer preserves unsent public comments/replies and survives its own detail refresh or target removal. Five locales, keyboard/large text and twelve synthetic captures cover three widths and both themes; see [private replies](mr-private-review-replies.md). Commit/image/file draft creation remains separate; no atomic, durable recovery or live-instance/device guarantee is claimed. [#624](https://github.com/Theorvane/LabFox/issues/624) adds the private commit-text draft API/repository foundation: exact commit/body/original text position, matching head SHA, strict private acknowledgement and captured author/global MR validation, with no redirect/auth replay/retry. No user action is exposed; guarded commit selection, membership/freshness, shared reservations and recovery remain follow-ups. See [commit-text draft API](mr-commit-text-draft-api.md). Image/file creation and MW-07 remain in progress. [#626](https://github.com/Theorvane/LabFox/issues/626) adds MR-scoped commit reads and complete advertised-page traversal, preserving unknown/root/ordered parent metadata. Strict SHA-1 identities, offset and same-resource Link cursor checks, cross-page duplicate rejection, immutable results and origin currency guards prevent partial or obsolete membership exposure. Read-only OAuth refresh remains account-bound. See [MR commit membership reads](mr-commit-membership-reads.md). Original commit diff selection and guarded private-save/recovery UI remain follow-ups; no atomic snapshot or live-instance guarantee is claimed. [#628](https://github.com/Theorvane/LabFox/issues/628) adds fixed-SHA literal commit diff pages and guarded advertised-page traversal, retaining raw paths/text and unknown omission flags. Shared offset validation preserves MR membership and confirms unified format plus exact route echoes. Diff file limits can stop pagination before full coverage; no original coordinate, membership or private-write preflight is inferred. See [original commit diff reads](original-commit-diff-reads.md). Authoritative fork/parent context and guarded literal selection/save/recovery UI remain follow-ups. [#630](https://github.com/Theorvane/LabFox/issues/630) preserves separate nullable fork project identities and adds a fresh captured MR detail context read. Strict integer and route/global identity checks reject malformed or mismatched responses; unknown target context is never inferred, while unknown source metadata stays unknown. Currency guards discard obsolete results and typed failures; read-only authentication remains captured. See [MR commit review context](mr-commit-review-context.md). Original parent/reference validation and guarded literal selection/save/recovery UI remain follow-ups; no atomic snapshot, original-coordinate or write-eligibility guarantee is claimed. [#632](https://github.com/Theorvane/LabFox/issues/632) adds strict fixed-head commit detail and guarded captured-account reads. Exact SHA and ordered nullable parent checks preserve unknown/root/merge metadata without synthesizing references; status-first failures, disabled redirects and obsolete-result isolation retain honest context. General branch/tag reads remain compatible. See [fixed commit review reads](fixed-commit-review-reads.md). Original reference/coordinate validation and guarded private-save/recovery UI remain follow-ups; no membership, atomic snapshot or write-eligibility guarantee is inferred. Captured original commit aggregation follows in #634 ([contract](mr-original-commit-context.md)); guarded literal coordinates and private-save UI remain in progress. Strict literal original diff validation and endpoint matching follow in #636 ([contract](literal-original-diff.md)); original refs and guarded private-save UI remain in progress. Captured single-parent original review snapshots and full-file selection fingerprints follow in #638 ([contract](original-review-snapshots.md)); guarded private-save UI remains in progress. [#640](https://github.com/Theorvane/labfox/issues/640) connects original selections to the shared discussion write reservation: fresh complete-fingerprint reconfirmation, captured-client parent availability, one private commit draft write, immediate account/provider/reload cancellation, strict acknowledgements and settled all-page read-only inspection without replay. Original-commit review UI remains a subsequent slice. | | MW-08 | P1 | CI/CD configuration: pipeline editor, variables, triggers, schedule editing, and execution history. | In progress | [#423](https://github.com/Theorvane/labfox/issues/423), [PR #424](https://github.com/Theorvane/labfox/pull/424) cover timing editing; [#425](https://github.com/Theorvane/labfox/issues/425), [PR #426](https://github.com/Theorvane/labfox/pull/426) cover deletion; [#427](https://github.com/Theorvane/labfox/issues/427), [PR #428](https://github.com/Theorvane/labfox/pull/428) cover creation; [#429](https://github.com/Theorvane/labfox/issues/429), [PR #430](https://github.com/Theorvane/labfox/pull/430) cover ownership transfer; [#431](https://github.com/Theorvane/labfox/issues/431), [PR #432](https://github.com/Theorvane/labfox/pull/432) cover ref/active editing. These slices await review or maintainer merge. [#433](https://github.com/Theorvane/labfox/issues/433), [PR #434](https://github.com/Theorvane/labfox/pull/434) cover schedule-specific execution history in review: newest-first header pagination, retained rows/cursor on failed next pages, retry, run-now/refresh reload with inline errors, stale-request protection, and identifier-only pipeline navigation. History filters, variables/inputs, pipeline configuration editing, and triggers remain separate slices. | | MW-09 | P1 | Wiki creation/editing/deletion and history with conflict handling. | In progress | [#351](https://github.com/Theorvane/labfox/issues/351), [PR #352](https://github.com/Theorvane/labfox/pull/352) shipped Markdown page creation; [#353](https://github.com/Theorvane/labfox/issues/353), [PR #354](https://github.com/Theorvane/labfox/pull/354) shipped editing with best-effort stale-draft detection; [#373](https://github.com/Theorvane/labfox/issues/373), [PR #374](https://github.com/Theorvane/labfox/pull/374) shipped deletion with confirmation and best-effort stale-page detection; [#375](https://github.com/Theorvane/labfox/issues/375), [PR #376](https://github.com/Theorvane/labfox/pull/376) shipped Markdown template selection. [#377](https://github.com/Theorvane/labfox/issues/377), [PR #378](https://github.com/Theorvane/labfox/pull/378) cover template listing and creation in review. Page history and atomic conflict protection remain. The documented wiki REST API can retrieve a page by version SHA but does not list page history. | | MW-10 | P1 | Snippet creation/editing/deletion, files, and visibility. | In progress | [#355](https://github.com/Theorvane/labfox/issues/355), [PR #356](https://github.com/Theorvane/labfox/pull/356) shipped single-file project snippet creation; [#357](https://github.com/Theorvane/labfox/issues/357), [PR #358](https://github.com/Theorvane/labfox/pull/358) shipped deletion with confirmation; [#359](https://github.com/Theorvane/labfox/issues/359), [PR #360](https://github.com/Theorvane/labfox/pull/360) shipped title and description editing; [#361](https://github.com/Theorvane/labfox/issues/361), [PR #362](https://github.com/Theorvane/labfox/pull/362) shipped single-file content editing; [#363](https://github.com/Theorvane/labfox/issues/363), [PR #364](https://github.com/Theorvane/labfox/pull/364) shipped private/public visibility editing; [#365](https://github.com/Theorvane/labfox/issues/365), [PR #366](https://github.com/Theorvane/labfox/pull/366) shipped multi-file content editing; [#367](https://github.com/Theorvane/labfox/issues/367), [PR #368](https://github.com/Theorvane/labfox/pull/368) shipped adding a file; [#369](https://github.com/Theorvane/labfox/issues/369), [PR #370](https://github.com/Theorvane/labfox/pull/370) shipped deleting a file; [#371](https://github.com/Theorvane/labfox/issues/371), [PR #372](https://github.com/Theorvane/labfox/pull/372) shipped moving and renaming a file. Broader visibility management remains. | diff --git a/docs/original-review-snapshots.md b/docs/original-review-snapshots.md index eefb453f..58afac52 100644 --- a/docs/original-review-snapshots.md +++ b/docs/original-review-snapshots.md @@ -10,10 +10,16 @@ This slice supports one explicitly reported, canonical, distinct parent only. Th This is an independently implemented interpretation of GitLab's original commit convention, not a new field returned by the REST API. The [Commit model](https://github.com/gitlabhq/gitlabhq/blob/master/app/models/commit.rb) constructs original diff refs using the parent (or repository blank ref for root) and selected head. [Git commit handling](https://github.com/gitlabhq/gitlabhq/blob/master/lib/gitlab/git/commit.rb) reads the first parent and describes the original diff as the first-parent diff. [DiffRefs](https://github.com/gitlabhq/gitlabhq/blob/master/lib/gitlab/diff/diff_refs.rb) defaults start to base. This slice conservatively avoids root and multi-parent choices. No server implementation was copied. -The [Draft Notes API](https://docs.gitlab.com/api/draft_notes/) documents commit association and positioned drafts, but its generic MR position description does not establish every original-commit case. No live GitLab version or self-hosted compatibility guarantee is claimed. The local snapshot is not a write authorization or atomic server snapshot; parent availability and original references must still be confirmed within the guarded private-save workflow. +The [Draft Notes API](https://docs.gitlab.com/api/draft_notes/) documents commit association and positioned drafts, but its generic MR position description does not establish every original-commit case. No live GitLab version or self-hosted compatibility guarantee is claimed. The local snapshot is not a write authorization or atomic server snapshot; the guarded private-save workflow below separately confirms parent availability and original references. ## Captured selections -`MrOriginalReviewSelection` stores the canonical position, MR global/per-project identity, target/source project and branches, plus the entire literal file DTO. Reconfirmation rejects changed origin, commit/parent references, metadata or raw text even when the selected row itself is unchanged. An unrelated MR title change does not invalidate a selection. The selection fingerprint is resource-bound; account/provider/session currency remains the controller and eventual write reservation's responsibility. +`MrOriginalReviewSelection` stores the canonical position, MR global/per-project identity, target/source project and branches, plus the entire literal file DTO. Reconfirmation rejects changed origin, commit/parent references, metadata or raw text even when the selected row itself is unchanged. An unrelated MR title change does not invalidate a selection. The selection fingerprint is resource-bound; account/provider/session currency remains the controller and write reservation's responsibility. -No private write, automatic retry, publication, persistence, route, UI or localization change is included. Connect the snapshot/selection session to the existing shared pending-review write reservation and settled read-only recovery before adding the original-commit private-save UI. Do not pass this snapshot directly to a write merely because it exposes local positions. +## Guarded private saves + +`MrDiscussionsController.savePendingCommitNote` accepts the captured original query and canonical position. It shares the discussion write/pagination reservation and captures the authenticated account, client, repositories, detail and original snapshot session. A fresh original snapshot must match the complete selection fingerprint. Fresh private detail must retain the global MR ID and target project. The same captured client reads the fixed parent in the target project before one private `createCommit` call. Every await and the final dispatch boundary check currency; reload, account/repository replacement and disposal cancel obsolete outcomes. View guards are checked at await boundaries. + +The acknowledgement must retain the author, global MR ID, exact Markdown, selected commit, canonical position and unpublished/non-resolution metadata. An uncertain dispatched write blocks another save until explicit read-only inspection waits for the actual request to settle and traverses all advertised private-note pages. Cancellation cannot undo an already dispatched server write. There is no automatic replay, public fallback, publication or analytics emission. + +These sequential reads are not an atomic server snapshot or a live-version compatibility guarantee. Root, unknown-parent and multiple-parent commits remain unselectable. The original-commit list, literal-row renderer and localized private-save UI remain the next connection; this controller command does not expose a user action yet.