mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-11-10 20:47:01 +01:00
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file-length / file length <= 250 lines (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
New optional note field created_by (kuhy|claude), stored like by/repo and omitted when unset so older builds stay wire-compatible. App UI stamps kuhy; MCP add_task and the CLI stamp claude. list/get/search output includes it. Human notes sort above Claude's; Claude notes are hidden behind a per-device eye toggle. CLI --backfill-created-by [--dry-run] classifies old notes by the node that wrote created_at, writing only the created_by field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
108 lines
3.3 KiB
Dart
108 lines
3.3 KiB
Dart
/// The one-open-note-per-repo invariant. Single add path for the MCP
|
|
/// `add_task` and the CLI `--repo` add; callers persist and push.
|
|
library;
|
|
|
|
import 'package:todo/data/note.dart';
|
|
import 'package:todo/data/note_repository.dart';
|
|
import 'package:uuid/uuid.dart';
|
|
|
|
/// Statuses that count as open.
|
|
const Set<Status> kOpenStatuses = {Status.todo, Status.inProgress};
|
|
|
|
/// The repo note's title.
|
|
String repoNoteTitle(String repo) => '${repo.split('/').last} todos';
|
|
|
|
/// A `new_note` task's own note title: `<name> todos: <title>`.
|
|
String separateNoteTitle(String repo, String title) =>
|
|
'${repoNoteTitle(repo)}: ${title.trim()}';
|
|
|
|
/// Whether [note] is a `new_note` note, which the shared add path and the
|
|
/// one-time merge leave alone.
|
|
bool isSeparateNote(Note note) {
|
|
final repo = note.repo;
|
|
return repo != null &&
|
|
splitNote(note.text).title.startsWith('${repoNoteTitle(repo)}: ');
|
|
}
|
|
|
|
/// Joins two checks with `&&`; either may be absent.
|
|
String? composeDoneCheck(String? older, String? newer) {
|
|
final a = (older ?? '').trim();
|
|
final b = (newer ?? '').trim();
|
|
if (a.isEmpty) return b.isEmpty ? null : b;
|
|
return b.isEmpty ? a : '( $a ) && ( $b )';
|
|
}
|
|
|
|
/// A `## title` section.
|
|
String repoSection(String title, String body) {
|
|
final text = body.trim();
|
|
return text.isEmpty ? '## $title' : '## $title\n$text';
|
|
}
|
|
|
|
/// The higher priority.
|
|
Priority maxPriority(Priority a, Priority b) => a.value >= b.value ? a : b;
|
|
|
|
/// Title line and rest of a note's text.
|
|
({String title, String body}) splitNote(String text) {
|
|
final lines = text.split('\n');
|
|
final first = lines.indexWhere((l) => l.trim().isNotEmpty);
|
|
if (first < 0) return (title: '', body: '');
|
|
return (
|
|
title: lines[first].trim().replaceFirst(RegExp(r'^#+\s*'), ''),
|
|
body: lines.skip(first + 1).join('\n').trim(),
|
|
);
|
|
}
|
|
|
|
/// Oldest first; ties broken by id so the choice is deterministic.
|
|
int byAge(Note a, Note b) {
|
|
final c = a.createdAt.compareTo(b.createdAt);
|
|
return c != 0 ? c : a.id.compareTo(b.id);
|
|
}
|
|
|
|
/// The note to save and whether it was appended.
|
|
typedef RepoAdd = ({Note note, bool appended});
|
|
|
|
/// Appends the task to [repo]'s oldest open shared note, or builds that
|
|
/// note; separate (`new_note`) notes are never joined.
|
|
Future<RepoAdd> addRepoTask(
|
|
NoteRepository repository, {
|
|
required String repo,
|
|
required String title,
|
|
String body = '',
|
|
String? doneCheck,
|
|
Priority priority = Priority.medium,
|
|
String? by,
|
|
String? createdBy,
|
|
}) async {
|
|
final section = repoSection(title, body);
|
|
final open = (await repository.listNotes(
|
|
filter: NoteFilter(statuses: kOpenStatuses, repo: repo),
|
|
)).where((n) => !isSeparateNote(n)).toList();
|
|
if (open.isNotEmpty) {
|
|
final existing = (open..sort(byAge)).first;
|
|
return (
|
|
note: existing.copyWith(
|
|
text: '${existing.text}\n\n$section',
|
|
doneCheck: composeDoneCheck(existing.doneCheck, doneCheck),
|
|
priority: maxPriority(existing.priority, priority),
|
|
),
|
|
appended: true,
|
|
);
|
|
}
|
|
final now = DateTime.now();
|
|
return (
|
|
note: Note(
|
|
id: const Uuid().v4(),
|
|
text: '${repoNoteTitle(repo)}\n\n$section',
|
|
priority: priority,
|
|
status: Status.todo,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
repo: repo,
|
|
doneCheck: composeDoneCheck(null, doneCheck),
|
|
by: by,
|
|
createdBy: createdBy,
|
|
),
|
|
appended: false,
|
|
);
|
|
}
|