feat(learn): Learn's agent, an MCP server suggesting in shared studysets
Some checks failed
CI / check (push) Failing after 1m56s
Some checks failed
CI / check (push) Failing after 1m56s
Halkyon's ADR 0044, step 6c (D7, D49 to D52). apps/learn/halkyon_learn_agent is one executable, `halkyon-agent --instance URL sign-in | sync | mcp`: - sign-in keeps the token read from stdin, signs in with it, makes the agent's key pair on its first sign-in, keeps it before setting the public halves on the register (agents.setKeys), and syncs; - it opens its data as the app's _openPersonData does, on halkyon_learn_core and halkyon_content: its own SQLite database, the sync's space router, its keys and those of the spaces shared with it, and the repositories of studysets, cards, notes, paths, files and feedback; - mcp serves ten tools over stdio (dart_mcp): list, read and search the studysets shared with it, suggest a change or a new card or note, report, list a studyset's suggestions and reply. Each syncs first and a suggestion again at once; a refusal is the tool's error; - its files live in $XDG_CONFIG_HOME/halkyon-agent/<host>, 0700 and 0600, refused when others may read them (D50); - tool/install.sh builds it with `dart build cli` (libsodium and SQLite bundled) and copies it to its user on the host, the build before kept (D52). The synced gateways of learning paths and files, and the fakes of the core's gateways, move into halkyon_learn_core (its testing.dart), which the app's tests and the agent's share. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
72cac92365
commit
54e212a51c
103 changed files with 2925 additions and 314 deletions
|
|
@ -1,132 +0,0 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_learn_client/halkyon_learn_client.dart' as protocol;
|
||||
import 'package:halkyon_learn_core/core/asset/data/asset_gateway.dart';
|
||||
import 'package:halkyon_learn_core/core/content/payload.dart';
|
||||
import 'package:halkyon_learn_core/core/privacy/data/privacy.dart';
|
||||
import 'package:serverpod_offline_sync_client/offline_sync.dart';
|
||||
|
||||
/// [AssetGateway] on the person's local database, which the sync engine keeps
|
||||
/// in step with their other devices. A file of a private studyset has its
|
||||
/// row's payload sealed with its tree's key ([Privacy], ADR 0019).
|
||||
class SyncAssetGateway(
|
||||
final OfflineSyncDatabaseSession _db,
|
||||
final SyncService _sync,
|
||||
final Privacy _privacy,
|
||||
) implements AssetGateway {
|
||||
@override
|
||||
Future<AssetRow?> byId(String id) async {
|
||||
final protocol.UuidValue uuid;
|
||||
try {
|
||||
uuid = protocol.UuidValue.withValidation(id);
|
||||
} on FormatException {
|
||||
// Content can name anything; a name that is no id names no row.
|
||||
return null;
|
||||
}
|
||||
await _privacy.keys.refresh();
|
||||
return _row(await protocol.Asset.db.findById(_db, uuid));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AssetRow?> bySha256(String sha256) async {
|
||||
await _privacy.keys.refresh();
|
||||
return _row(
|
||||
await protocol.Asset.db.findFirstRow(
|
||||
_db,
|
||||
where: (t) => t.sha256.equals(sha256),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<AssetRow>> all() async {
|
||||
await _privacy.keys.refresh();
|
||||
return [
|
||||
for (final asset in await protocol.Asset.db.find(_db)) ?_row(asset),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
String newId() => Privacy.newId().uuid;
|
||||
|
||||
@override
|
||||
Future<String> insert({
|
||||
required String id,
|
||||
required String sha256,
|
||||
required String mimeType,
|
||||
required int byteSize,
|
||||
required ByteData payload,
|
||||
String? keyId,
|
||||
}) async {
|
||||
final sealed = _privacy.seal(
|
||||
payload,
|
||||
keyId: keyId,
|
||||
table: 'asset',
|
||||
bound: '',
|
||||
id: id,
|
||||
item: 'asset:$id',
|
||||
);
|
||||
final asset = await protocol.Asset.db.insertRow(
|
||||
_db,
|
||||
protocol.Asset(
|
||||
id: protocol.UuidValue.withValidation(id),
|
||||
sha256: sha256,
|
||||
mimeType: mimeType,
|
||||
byteSize: byteSize,
|
||||
keyId: sealed.keyId,
|
||||
nonce: sealed.nonce,
|
||||
payload: sealed.payload,
|
||||
),
|
||||
);
|
||||
_sync.requestSync();
|
||||
return asset.id!.uuid;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete(String id) async {
|
||||
final uuid = protocol.UuidValue.withValidation(id);
|
||||
if (await protocol.Asset.db.findById(_db, uuid) case final asset?) {
|
||||
// The name it came with is content (ADR 0002, rule 2).
|
||||
await protocol.Asset.db.updateRow(
|
||||
_db,
|
||||
asset.copyWith(payload: redactedPayload),
|
||||
columns: (t) => [t.payload],
|
||||
);
|
||||
await protocol.Asset.db.deleteWhere(
|
||||
_db,
|
||||
where: (t) => t.id.equals(uuid),
|
||||
);
|
||||
_sync.requestSync();
|
||||
}
|
||||
}
|
||||
|
||||
AssetRow? _row(protocol.Asset? asset) {
|
||||
if (asset == null) {
|
||||
return null;
|
||||
}
|
||||
// A file this device saw sealed never counts as readable again (the
|
||||
// reading of 22 Oct): a readable row in its place nobody signed is
|
||||
// locked.
|
||||
final opened = _privacy.openItem(
|
||||
(keyId: asset.keyId, nonce: asset.nonce, payload: asset.payload),
|
||||
table: 'asset',
|
||||
bound: '',
|
||||
id: asset.id!.uuid,
|
||||
item: 'asset:${asset.id!.uuid}',
|
||||
treeKey: null,
|
||||
);
|
||||
return (
|
||||
id: asset.id!.uuid,
|
||||
sha256: asset.sha256,
|
||||
mimeType: asset.mimeType,
|
||||
byteSize: asset.byteSize,
|
||||
payload: switch (opened) {
|
||||
Readable(:final payload) => payload,
|
||||
Locked() => redactedPayload,
|
||||
},
|
||||
keyId: asset.keyId,
|
||||
locked: opened is Locked,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import 'package:halkyon_app/halkyon_app.dart';
|
|||
import 'package:halkyon_learn_client/halkyon_learn_client.dart'
|
||||
show Client, PublishRefusal, PublishRefusedException, UuidValue;
|
||||
import 'package:halkyon_learn_core/core/asset/data/asset_repository.dart';
|
||||
import 'package:halkyon_learn_core/core/asset/data/sync_asset_gateway.dart';
|
||||
import 'package:halkyon_learn_core/core/privacy/data/content_keys.dart';
|
||||
import 'package:halkyon_learn_core/core/privacy/data/privacy.dart';
|
||||
import 'package:halkyon_learn_core/core/privacy/data/privacy_ledger.dart';
|
||||
|
|
@ -21,6 +22,7 @@ import 'package:halkyon_learn_core/feature/feedback/data/feedback_repository.dar
|
|||
import 'package:halkyon_learn_core/feature/feedback/data/serverpod_feedback_gateway.dart';
|
||||
import 'package:halkyon_learn_core/feature/feedback/data/sync_feedback_gateway.dart';
|
||||
import 'package:halkyon_learn_core/feature/learning_path/data/learning_path_repository.dart';
|
||||
import 'package:halkyon_learn_core/feature/learning_path/data/sync_learning_path_gateway.dart';
|
||||
import 'package:halkyon_learn_core/feature/note/data/note_deletions.dart';
|
||||
import 'package:halkyon_learn_core/feature/note/data/note_repository.dart';
|
||||
import 'package:halkyon_learn_core/feature/note/data/sync_note_gateway.dart';
|
||||
|
|
@ -31,7 +33,6 @@ import 'package:halkyon_learn_core/feature/studyset/model/studyset_tree.dart';
|
|||
import 'package:halkyon_learn_core/feature/tag/data/sync_tag_gateway.dart';
|
||||
import 'package:halkyon_learn_core/feature/tag/data/tag_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/asset/data/sound_player.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/asset/data/sync_asset_gateway.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/dependencies/person_dependencies.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/device_settings/data/device_settings_store.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/device_settings/model/device_settings.dart';
|
||||
|
|
@ -71,7 +72,6 @@ import 'package:halkyon_learn_flutter/src/feature/jellyfin/data/jellyfin_suggest
|
|||
import 'package:halkyon_learn_flutter/src/feature/kanji/data/stroke_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_deletions.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/path_progress_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/sync_learning_path_gateway.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/sync_path_answer_gateway.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pdf_source/data/pdf_deletions.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pdf_source/data/pdf_engine.dart';
|
||||
|
|
|
|||
|
|
@ -1,386 +0,0 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_learn_client/halkyon_learn_client.dart' as protocol;
|
||||
import 'package:halkyon_learn_core/core/content/payload.dart';
|
||||
import 'package:halkyon_learn_core/core/privacy/data/privacy.dart';
|
||||
import 'package:halkyon_learn_core/feature/learning_path/data/learning_path_gateway.dart';
|
||||
import 'package:serverpod_offline_sync_client/offline_sync.dart';
|
||||
|
||||
/// [LearningPathGateway] on the person's local database, which the sync
|
||||
/// engine keeps in step with their other devices. Step revisions record
|
||||
/// [_authorId] as their author. In a private studyset, paths and step
|
||||
/// revisions are sealed with their tree's key ([Privacy], ADR 0019); one
|
||||
/// that doesn't open here reads as blank.
|
||||
class SyncLearningPathGateway(
|
||||
final OfflineSyncDatabaseSession _db,
|
||||
final SyncService _sync,
|
||||
final String _authorId,
|
||||
final Privacy _privacy,
|
||||
) implements LearningPathGateway {
|
||||
@override
|
||||
Future<List<PathRow>> paths({String? studysetId}) async {
|
||||
await _privacy.keys.refresh();
|
||||
final treeKey = await _privacy.treeKeys();
|
||||
return [
|
||||
for (final path in await protocol.LearningPath.db.find(
|
||||
_db,
|
||||
where: studysetId == null
|
||||
? null
|
||||
: (t) => t.studysetId.equals(_uuid(studysetId)),
|
||||
))
|
||||
(
|
||||
id: path.id!.uuid,
|
||||
studysetId: path.studysetId.uuid,
|
||||
position: path.position,
|
||||
payload: _opened(
|
||||
'learning_path',
|
||||
path.id!.uuid,
|
||||
(keyId: path.keyId, nonce: path.nonce, payload: path.payload),
|
||||
bound: 'studyset:${path.studysetId.uuid}',
|
||||
item: 'path:${path.id!.uuid}',
|
||||
treeKey: treeKey(path.studysetId.uuid),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// [envelope]'s payload opened as [item]'s, [bound] to the row it
|
||||
/// belongs to, in a tree with [treeKey]; blank where it doesn't open here,
|
||||
/// or isn't trusted there.
|
||||
ByteData _opened(
|
||||
String table,
|
||||
String id,
|
||||
Envelope envelope, {
|
||||
required String bound,
|
||||
required String item,
|
||||
required String? treeKey,
|
||||
}) => switch (_privacy.openItem(
|
||||
envelope,
|
||||
table: table,
|
||||
id: id,
|
||||
bound: bound,
|
||||
item: item,
|
||||
treeKey: treeKey,
|
||||
)) {
|
||||
Readable(:final payload) => payload,
|
||||
Locked() => redactedPayload,
|
||||
};
|
||||
|
||||
@override
|
||||
Future<String> insertPath({
|
||||
required String studysetId,
|
||||
required String position,
|
||||
required ByteData payload,
|
||||
}) async {
|
||||
final id = Privacy.newId();
|
||||
final sealed = _privacy.seal(
|
||||
payload,
|
||||
keyId: await _privacy.keyToWriteIn(studysetId),
|
||||
table: 'learning_path',
|
||||
id: id.uuid,
|
||||
bound: 'studyset:$studysetId',
|
||||
item: 'path:${id.uuid}',
|
||||
);
|
||||
final path = await protocol.LearningPath.db.insertRow(
|
||||
_db,
|
||||
protocol.LearningPath(
|
||||
id: id,
|
||||
studysetId: _uuid(studysetId),
|
||||
position: position,
|
||||
keyId: sealed.keyId,
|
||||
nonce: sealed.nonce,
|
||||
payload: sealed.payload,
|
||||
),
|
||||
);
|
||||
_sync.requestSync();
|
||||
return path.id!.uuid;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updatePathPayload(String id, ByteData payload) async {
|
||||
if (await protocol.LearningPath.db.findById(_db, _uuid(id))
|
||||
case final path?) {
|
||||
final sealed = _privacy.seal(
|
||||
payload,
|
||||
keyId: await _privacy.keyOfPath(id),
|
||||
table: 'learning_path',
|
||||
id: id,
|
||||
bound: 'studyset:${path.studysetId.uuid}',
|
||||
item: 'path:$id',
|
||||
);
|
||||
// Only the named columns change (ADR 0001, rule 1).
|
||||
await protocol.LearningPath.db.updateRow(
|
||||
_db,
|
||||
path.copyWith(
|
||||
keyId: sealed.keyId,
|
||||
nonce: sealed.nonce,
|
||||
payload: sealed.payload,
|
||||
),
|
||||
columns: (t) => [t.keyId, t.nonce, t.payload],
|
||||
);
|
||||
_sync.requestSync();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updatePathPosition(String id, String position) async {
|
||||
if (await protocol.LearningPath.db.findById(_db, _uuid(id))
|
||||
case final path?) {
|
||||
await protocol.LearningPath.db.updateRow(
|
||||
_db,
|
||||
path.copyWith(position: position),
|
||||
columns: (t) => [t.position],
|
||||
);
|
||||
_sync.requestSync();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deletePath(String id) async {
|
||||
await protocol.LearningPathTag.db.deleteWhere(
|
||||
_db,
|
||||
where: (t) => t.pathId.equals(_uuid(id)),
|
||||
);
|
||||
await protocol.LearningPath.db.deleteWhere(
|
||||
_db,
|
||||
where: (t) => t.id.equals(_uuid(id)),
|
||||
);
|
||||
_sync.requestSync();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<StepRow>> steps(Set<String> pathIds) async {
|
||||
if (pathIds.isEmpty) {
|
||||
return const [];
|
||||
}
|
||||
return [
|
||||
for (final step in await protocol.LearningPathStep.db.find(
|
||||
_db,
|
||||
where: (t) => t.pathId.inSet({for (final id in pathIds) _uuid(id)}),
|
||||
))
|
||||
(
|
||||
id: step.id!.uuid,
|
||||
pathId: step.pathId.uuid,
|
||||
position: step.position,
|
||||
kind: step.kind,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<StepRevisionRow>> revisions(Set<String> stepIds) async {
|
||||
if (stepIds.isEmpty) {
|
||||
return const [];
|
||||
}
|
||||
await _privacy.keys.refresh();
|
||||
final ids = {for (final id in stepIds) _uuid(id)};
|
||||
final treeKey = await _privacy.treeKeys();
|
||||
final pathOf = {
|
||||
for (final step in await protocol.LearningPathStep.db.find(
|
||||
_db,
|
||||
where: (t) => t.id.inSet(ids),
|
||||
))
|
||||
step.id!.uuid: step.pathId,
|
||||
};
|
||||
final studysetOf = pathOf.isEmpty
|
||||
? const <String, String>{}
|
||||
: {
|
||||
for (final path in await protocol.LearningPath.db.find(
|
||||
_db,
|
||||
where: (t) => t.id.inSet(<protocol.UuidValue>{...pathOf.values}),
|
||||
))
|
||||
path.id!.uuid: path.studysetId.uuid,
|
||||
};
|
||||
String? treeKeyOf(String stepId) =>
|
||||
switch (studysetOf[pathOf[stepId]?.uuid]) {
|
||||
final studyset? => treeKey(studyset),
|
||||
null => null,
|
||||
};
|
||||
return [
|
||||
for (final revision in await protocol.LearningPathStepRevision.db.find(
|
||||
_db,
|
||||
where: (t) => t.stepId.inSet(ids),
|
||||
))
|
||||
(
|
||||
id: revision.id!.uuid,
|
||||
stepId: revision.stepId.uuid,
|
||||
parents: {for (final parent in revision.parents) parent.uuid},
|
||||
payload: _opened(
|
||||
'learning_path_step_revision',
|
||||
revision.id!.uuid,
|
||||
(
|
||||
keyId: revision.keyId,
|
||||
nonce: revision.nonce,
|
||||
payload: revision.payload,
|
||||
),
|
||||
bound: 'step:${revision.stepId.uuid}',
|
||||
item: 'step:${revision.stepId.uuid}',
|
||||
treeKey: treeKeyOf(revision.stepId.uuid),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<({String stepId, String revisionId})> insertStep({
|
||||
required String pathId,
|
||||
required String position,
|
||||
required String kind,
|
||||
required ByteData payload,
|
||||
}) async {
|
||||
// Refused before anything is written where this device may write
|
||||
// nothing (the final reading of 22 Oct).
|
||||
await _privacy.keyOfPath(pathId);
|
||||
final step = await protocol.LearningPathStep.db.insertRow(
|
||||
_db,
|
||||
protocol.LearningPathStep(
|
||||
pathId: _uuid(pathId),
|
||||
position: position,
|
||||
kind: kind,
|
||||
),
|
||||
);
|
||||
final revisionId = await insertRevision(
|
||||
stepId: step.id!.uuid,
|
||||
parents: const {},
|
||||
payload: payload,
|
||||
);
|
||||
return (stepId: step.id!.uuid, revisionId: revisionId);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<String>> insertSteps(
|
||||
String pathId,
|
||||
List<NewStepRow> steps,
|
||||
) async {
|
||||
if (steps.isEmpty) {
|
||||
return const [];
|
||||
}
|
||||
final keyId = await _privacy.keyOfPath(pathId);
|
||||
final rows = await protocol.LearningPathStep.db.insert(_db, [
|
||||
for (final step in steps)
|
||||
protocol.LearningPathStep(
|
||||
pathId: _uuid(pathId),
|
||||
position: step.position,
|
||||
kind: step.kind,
|
||||
),
|
||||
]);
|
||||
// The batch's positions are distinct, so they tell the stored rows apart
|
||||
// whatever order they come back in.
|
||||
final idOf = {for (final row in rows) row.position: row.id!};
|
||||
await protocol.LearningPathStepRevision.db.insert(_db, [
|
||||
for (final step in steps)
|
||||
_newRevision(idOf[step.position]!, const [], step.payload, keyId),
|
||||
]);
|
||||
_sync.requestSync();
|
||||
return [for (final step in steps) idOf[step.position]!.uuid];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> insertRevision({
|
||||
required String stepId,
|
||||
required Set<String> parents,
|
||||
required ByteData payload,
|
||||
}) async {
|
||||
final revision = await protocol.LearningPathStepRevision.db.insertRow(
|
||||
_db,
|
||||
_newRevision(
|
||||
_uuid(stepId),
|
||||
parents,
|
||||
payload,
|
||||
await _privacy.keyOfStep(stepId),
|
||||
),
|
||||
);
|
||||
_sync.requestSync();
|
||||
return revision.id!.uuid;
|
||||
}
|
||||
|
||||
/// A new revision of [stepId] with [payload], sealed with [keyId].
|
||||
protocol.LearningPathStepRevision _newRevision(
|
||||
protocol.UuidValue stepId,
|
||||
Iterable<String> parents,
|
||||
ByteData payload,
|
||||
String? keyId,
|
||||
) {
|
||||
final id = Privacy.newId();
|
||||
final sealed = _privacy.seal(
|
||||
payload,
|
||||
keyId: keyId,
|
||||
table: 'learning_path_step_revision',
|
||||
id: id.uuid,
|
||||
bound: 'step:${stepId.uuid}',
|
||||
item: 'step:${stepId.uuid}',
|
||||
);
|
||||
return protocol.LearningPathStepRevision(
|
||||
id: id,
|
||||
stepId: stepId,
|
||||
parents: [for (final parent in parents) _uuid(parent)],
|
||||
authorId: _uuid(_authorId),
|
||||
keyId: sealed.keyId,
|
||||
nonce: sealed.nonce,
|
||||
payload: sealed.payload,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateStepPosition(String id, String position) async {
|
||||
if (await protocol.LearningPathStep.db.findById(_db, _uuid(id))
|
||||
case final step?) {
|
||||
await protocol.LearningPathStep.db.updateRow(
|
||||
_db,
|
||||
step.copyWith(position: position),
|
||||
columns: (t) => [t.position],
|
||||
);
|
||||
_sync.requestSync();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> blank({String? stepId, String? pathId}) async {
|
||||
if (stepId != null) {
|
||||
final revisions = await protocol.LearningPathStepRevision.db.find(
|
||||
_db,
|
||||
where: (t) => t.stepId.equals(_uuid(stepId)),
|
||||
);
|
||||
// Column-scoped updates, which sync like any other edit (ADR 0002,
|
||||
// rule 2).
|
||||
await protocol.LearningPathStepRevision.db.update(
|
||||
_db,
|
||||
[
|
||||
for (final revision in revisions)
|
||||
revision.copyWith(payload: redactedPayload),
|
||||
],
|
||||
columns: (t) => [t.payload],
|
||||
);
|
||||
}
|
||||
if (pathId != null) {
|
||||
await updatePathPayload(pathId, redactedPayload);
|
||||
}
|
||||
_sync.requestSync();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteStep(String id) async {
|
||||
final step = _uuid(id);
|
||||
// Its answers go too: no content, but personal study data that would
|
||||
// otherwise stay on the server as hidden rows.
|
||||
await protocol.LearningPathAnswer.db.deleteWhere(
|
||||
_db,
|
||||
where: (t) => t.stepId.equals(step),
|
||||
);
|
||||
await protocol.LearningPathStepRevision.db.deleteWhere(
|
||||
_db,
|
||||
where: (t) => t.stepId.equals(step),
|
||||
);
|
||||
await protocol.LearningPathStep.db.deleteWhere(
|
||||
_db,
|
||||
where: (t) => t.id.equals(step),
|
||||
);
|
||||
_sync.requestSync();
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<void> get remoteChanges => _sync.merges;
|
||||
|
||||
protocol.UuidValue _uuid(String id) => protocol.UuidValue.withValidation(id);
|
||||
}
|
||||
|
|
@ -12,19 +12,15 @@ import 'package:halkyon_learn_core/feature/note/model/note_content.dart';
|
|||
import 'package:halkyon_learn_core/feature/studyset/data/studyset_payload.dart';
|
||||
import 'package:halkyon_learn_core/feature/studyset/model/studyset_content.dart';
|
||||
import 'package:halkyon_learn_core/feature/tag/data/tag_repository.dart';
|
||||
import 'package:halkyon_learn_core/testing.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/model/pomodoro_profile.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_repository.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
import '../support/fake_card_gateway.dart';
|
||||
import '../support/fake_discussion_gateway.dart';
|
||||
import '../support/fake_learning_path_gateway.dart';
|
||||
import '../support/fake_note_gateway.dart';
|
||||
import '../support/fake_settings_gateway.dart';
|
||||
import '../support/fake_studyset_gateway.dart';
|
||||
import '../support/fake_tag_gateway.dart';
|
||||
import '../support/pump_app.dart';
|
||||
import '../support/test_dependencies.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -5,10 +5,9 @@ import 'package:flutter_test/flutter_test.dart';
|
|||
import 'package:halkyon_app/testing.dart'
|
||||
show FakeFileServerGateway, MemoryFileCache, sha256Of;
|
||||
import 'package:halkyon_learn_core/core/asset/data/asset_repository.dart';
|
||||
import 'package:halkyon_learn_core/testing.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/asset/bloc/sound_bloc.dart';
|
||||
|
||||
import '../../../support/fake_asset_gateway.dart';
|
||||
import '../../../support/fake_asset_sealing.dart';
|
||||
import '../../../support/fake_sound_player.dart';
|
||||
|
||||
void main() {
|
||||
|
|
|
|||
|
|
@ -8,9 +8,7 @@ import 'package:halkyon_app/testing.dart'
|
|||
import 'package:halkyon_learn_core/core/asset/data/asset_payload.dart';
|
||||
import 'package:halkyon_learn_core/core/asset/data/asset_repository.dart';
|
||||
import 'package:halkyon_learn_core/core/asset/model/stored_file.dart';
|
||||
|
||||
import '../../../support/fake_asset_gateway.dart';
|
||||
import '../../../support/fake_asset_sealing.dart';
|
||||
import 'package:halkyon_learn_core/testing.dart';
|
||||
|
||||
void main() {
|
||||
late FakeAssetGateway rows;
|
||||
|
|
|
|||
|
|
@ -6,9 +6,7 @@ import 'package:halkyon_app/testing.dart'
|
|||
show FakeFileServerGateway, MemoryFileCache, sha256Of;
|
||||
import 'package:halkyon_learn_core/core/asset/data/asset_payload.dart';
|
||||
import 'package:halkyon_learn_core/core/asset/data/asset_repository.dart';
|
||||
|
||||
import '../../../support/fake_asset_gateway.dart';
|
||||
import '../../../support/fake_asset_sealing.dart';
|
||||
import 'package:halkyon_learn_core/testing.dart';
|
||||
|
||||
/// A private studyset's files (ADR 0019, D5): sealed with its tree's key for
|
||||
/// their row before they are hashed, uploaded and kept.
|
||||
|
|
|
|||
|
|
@ -7,14 +7,12 @@ import 'package:halkyon_app/testing.dart';
|
|||
import 'package:halkyon_editor/halkyon_editor.dart';
|
||||
import 'package:halkyon_learn_core/feature/studyset/data/studyset_payload.dart';
|
||||
import 'package:halkyon_learn_core/feature/studyset/model/studyset_content.dart';
|
||||
import 'package:halkyon_learn_core/testing.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/learn_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/l10n/app_languages.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
import '../../support/fake_asset_gateway.dart';
|
||||
import '../../support/fake_note_gateway.dart';
|
||||
import '../../support/fake_studyset_gateway.dart';
|
||||
import '../../support/pump_app.dart';
|
||||
import '../../support/test_dependencies.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -12,16 +12,13 @@ import 'package:halkyon_learn_core/feature/card/data/card_repository.dart';
|
|||
import 'package:halkyon_learn_core/feature/card/model/card_content.dart';
|
||||
import 'package:halkyon_learn_core/feature/studyset/data/studyset_payload.dart';
|
||||
import 'package:halkyon_learn_core/feature/studyset/model/studyset_content.dart';
|
||||
import 'package:halkyon_learn_core/testing.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/asset/widget/stored_file_image.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/file_blocks.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
import '../../support/fake_asset_gateway.dart';
|
||||
import '../../support/fake_asset_sealing.dart';
|
||||
import '../../support/fake_card_gateway.dart';
|
||||
import '../../support/fake_sound_player.dart';
|
||||
import '../../support/fake_studyset_gateway.dart';
|
||||
import '../../support/pump_app.dart';
|
||||
import '../../support/test_dependencies.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:halkyon_learn_core/feature/note/data/note_repository.dart';
|
|||
import 'package:halkyon_learn_core/feature/note/model/note_content.dart';
|
||||
import 'package:halkyon_learn_core/feature/studyset/data/studyset_payload.dart';
|
||||
import 'package:halkyon_learn_core/feature/studyset/model/studyset_content.dart';
|
||||
import 'package:halkyon_learn_core/testing.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/device_settings/model/device_settings.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/grammar/grammar_match.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/grammar/grammar_overlay.dart';
|
||||
|
|
@ -13,9 +14,7 @@ import 'package:halkyon_learn_flutter/src/feature/settings/model/person_settings
|
|||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
import '../../support/fake_grammar_gateway.dart';
|
||||
import '../../support/fake_note_gateway.dart';
|
||||
import '../../support/fake_settings_gateway.dart';
|
||||
import '../../support/fake_studyset_gateway.dart';
|
||||
import '../../support/pump_app.dart';
|
||||
import '../../support/test_dependencies.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@ import 'package:halkyon_learn_core/core/privacy/data/privacy_ledger.dart';
|
|||
import 'package:halkyon_learn_core/core/privacy/data/sync_content_key_gateway.dart';
|
||||
import 'package:halkyon_learn_core/core/privacy/data/tree_files.dart';
|
||||
import 'package:halkyon_learn_core/feature/card/data/sync_card_gateway.dart';
|
||||
import 'package:halkyon_learn_core/feature/learning_path/data/sync_learning_path_gateway.dart';
|
||||
import 'package:halkyon_learn_core/feature/note/data/sync_note_gateway.dart';
|
||||
import 'package:halkyon_learn_core/feature/studyset/data/sync_studyset_gateway.dart';
|
||||
import 'package:halkyon_learn_core/feature/tag/data/sync_tag_gateway.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/sync_learning_path_gateway.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/sync_path_answer_gateway.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pdf_source/data/sync_pdf_source_gateway.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/publish/data/publish_repository.dart';
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ import 'package:halkyon_learn_core/core/privacy/data/sync_content_key_gateway.da
|
|||
import 'package:halkyon_learn_core/core/privacy/data/tree_files.dart';
|
||||
import 'package:halkyon_learn_core/core/privacy/model/unsigned_edit.dart';
|
||||
import 'package:halkyon_learn_core/feature/card/data/sync_card_gateway.dart';
|
||||
import 'package:halkyon_learn_core/feature/learning_path/data/sync_learning_path_gateway.dart';
|
||||
import 'package:halkyon_learn_core/feature/studyset/data/sync_studyset_gateway.dart';
|
||||
import 'package:halkyon_learn_core/feature/tag/data/sync_tag_gateway.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/sync_learning_path_gateway.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/sync_path_answer_gateway.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/settings/data/sync_settings_gateway.dart';
|
||||
import 'package:serverpod_offline_sync_client/offline_sync.dart';
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:halkyon_learn_core/feature/card/data/card_repository.dart';
|
|||
import 'package:halkyon_learn_core/feature/card/model/card_content.dart';
|
||||
import 'package:halkyon_learn_core/feature/studyset/data/studyset_payload.dart';
|
||||
import 'package:halkyon_learn_core/feature/studyset/model/studyset_content.dart';
|
||||
import 'package:halkyon_learn_core/testing.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/home/widget/activity_heatmap.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/home/widget/home_screen.dart';
|
||||
|
|
@ -13,8 +14,6 @@ import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_scree
|
|||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
import '../../support/fake_card_gateway.dart';
|
||||
import '../../support/fake_studyset_gateway.dart';
|
||||
import '../../support/memory_device_settings_store.dart';
|
||||
import '../../support/pump_app.dart';
|
||||
import '../../support/test_dependencies.dart';
|
||||
|
|
|
|||
|
|
@ -4,12 +4,11 @@ import 'package:halkyon_learn_core/feature/card/data/card_repository.dart';
|
|||
import 'package:halkyon_learn_core/feature/card/model/card_content.dart';
|
||||
import 'package:halkyon_learn_core/feature/studyset/data/studyset_payload.dart';
|
||||
import 'package:halkyon_learn_core/feature/studyset/model/studyset_content.dart';
|
||||
import 'package:halkyon_learn_core/testing.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
import '../../support/fake_card_gateway.dart';
|
||||
import '../../support/fake_studyset_gateway.dart';
|
||||
import '../../support/pump_app.dart';
|
||||
import '../../support/test_dependencies.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import 'package:halkyon_learn_core/feature/note/data/note_repository.dart';
|
|||
import 'package:halkyon_learn_core/feature/studyset/data/studyset_repository.dart';
|
||||
import 'package:halkyon_learn_core/feature/studyset/model/studyset_content.dart';
|
||||
import 'package:halkyon_learn_core/feature/tag/data/tag_repository.dart';
|
||||
import 'package:halkyon_learn_core/testing.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/work/work_progress.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/anki/data/anki_importer.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/anki/data/anki_package.dart';
|
||||
|
|
@ -22,15 +23,8 @@ import 'package:halkyon_learn_flutter/src/feature/study/model/rating.dart';
|
|||
import 'package:halkyon_learn_flutter/src/feature/study/model/review.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_deletions.dart';
|
||||
|
||||
import '../../../support/fake_asset_gateway.dart';
|
||||
import '../../../support/fake_asset_sealing.dart';
|
||||
import '../../../support/fake_card_gateway.dart';
|
||||
import '../../../support/fake_learning_path_gateway.dart';
|
||||
import '../../../support/fake_note_gateway.dart';
|
||||
import '../../../support/fake_pdf_source_gateway.dart';
|
||||
import '../../../support/fake_review_gateway.dart';
|
||||
import '../../../support/fake_studyset_gateway.dart';
|
||||
import '../../../support/fake_tag_gateway.dart';
|
||||
import 'anki_package_test.dart' show fixture;
|
||||
|
||||
void main() {
|
||||
|
|
|
|||
|
|
@ -10,14 +10,12 @@ import 'package:halkyon_learn_core/feature/note/model/note_content.dart';
|
|||
import 'package:halkyon_learn_core/feature/studyset/data/studyset_payload.dart';
|
||||
import 'package:halkyon_learn_core/feature/studyset/data/studyset_repository.dart';
|
||||
import 'package:halkyon_learn_core/feature/studyset/model/studyset_content.dart';
|
||||
import 'package:halkyon_learn_core/testing.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/work_progress_view.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/bloc/content_tab_cubit.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
import '../../../support/fake_card_gateway.dart';
|
||||
import '../../../support/fake_note_gateway.dart';
|
||||
import '../../../support/fake_studyset_gateway.dart';
|
||||
import '../../../support/pump_app.dart';
|
||||
import '../../../support/test_dependencies.dart';
|
||||
import '../data/anki_package_test.dart' show fixture;
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import 'package:halkyon_learn_core/feature/studyset/model/exam_plan.dart';
|
|||
import 'package:halkyon_learn_core/feature/studyset/model/studyset_content.dart';
|
||||
import 'package:halkyon_learn_core/feature/studyset/model/studyset_tree.dart';
|
||||
import 'package:halkyon_learn_core/feature/tag/data/tag_repository.dart';
|
||||
import 'package:halkyon_learn_core/testing.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/work/work_progress.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/archive/data/archive_exporter.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/archive/data/archive_importer.dart';
|
||||
|
|
@ -40,18 +41,11 @@ import 'package:halkyon_learn_flutter/src/feature/study/model/review.dart';
|
|||
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_deletions.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
|
||||
import '../../support/fake_asset_gateway.dart';
|
||||
import '../../support/fake_asset_sealing.dart';
|
||||
import '../../support/fake_card_gateway.dart';
|
||||
import '../../support/fake_device_dictionaries.dart';
|
||||
import '../../support/fake_dictionary_gateway.dart';
|
||||
import '../../support/fake_learning_path_gateway.dart';
|
||||
import '../../support/fake_note_gateway.dart';
|
||||
import '../../support/fake_path_answer_gateway.dart';
|
||||
import '../../support/fake_pdf_source_gateway.dart';
|
||||
import '../../support/fake_review_gateway.dart';
|
||||
import '../../support/fake_studyset_gateway.dart';
|
||||
import '../../support/fake_tag_gateway.dart';
|
||||
|
||||
/// One person's repositories, on fake gateways of their own.
|
||||
final class _Person() {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import 'package:halkyon_learn_core/feature/studyset/data/studyset_payload.dart';
|
|||
import 'package:halkyon_learn_core/feature/studyset/data/studyset_repository.dart';
|
||||
import 'package:halkyon_learn_core/feature/studyset/model/studyset_content.dart';
|
||||
import 'package:halkyon_learn_core/feature/tag/data/tag_repository.dart';
|
||||
import 'package:halkyon_learn_core/testing.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/work/work_progress.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/archive/data/archive_importer.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/archive/data/archive_reader.dart';
|
||||
|
|
@ -33,18 +34,11 @@ import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.d
|
|||
import 'package:halkyon_learn_flutter/src/feature/study/model/rating.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_deletions.dart';
|
||||
|
||||
import '../../support/fake_asset_gateway.dart';
|
||||
import '../../support/fake_asset_sealing.dart';
|
||||
import '../../support/fake_card_gateway.dart';
|
||||
import '../../support/fake_device_dictionaries.dart';
|
||||
import '../../support/fake_dictionary_gateway.dart';
|
||||
import '../../support/fake_learning_path_gateway.dart';
|
||||
import '../../support/fake_note_gateway.dart';
|
||||
import '../../support/fake_path_answer_gateway.dart';
|
||||
import '../../support/fake_pdf_source_gateway.dart';
|
||||
import '../../support/fake_review_gateway.dart';
|
||||
import '../../support/fake_studyset_gateway.dart';
|
||||
import '../../support/fake_tag_gateway.dart';
|
||||
|
||||
/// The JSON a synced [payload] holds.
|
||||
Map<String, Object?> json(ByteData payload) =>
|
||||
|
|
|
|||
|
|
@ -3,14 +3,13 @@ import 'dart:async';
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_app/testing.dart';
|
||||
import 'package:halkyon_learn_core/feature/studyset/data/studyset_repository.dart';
|
||||
import 'package:halkyon_learn_core/testing.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/work_progress_view.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_screen.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
import '../../support/fake_card_gateway.dart';
|
||||
import '../../support/fake_dictionary_gateway.dart';
|
||||
import '../../support/fake_studyset_gateway.dart';
|
||||
import '../../support/pump_app.dart';
|
||||
import '../../support/test_dependencies.dart';
|
||||
import 'archive_test.dart' show japanese, zip;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:halkyon_learn_core/feature/card/data/card_repository.dart';
|
|||
import 'package:halkyon_learn_core/feature/card/model/card_content.dart';
|
||||
import 'package:halkyon_learn_core/feature/studyset/data/studyset_payload.dart';
|
||||
import 'package:halkyon_learn_core/feature/studyset/model/studyset_content.dart';
|
||||
import 'package:halkyon_learn_core/testing.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/device_settings/model/device_settings.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/assistant/data/web_search.dart';
|
||||
|
|
@ -17,8 +18,6 @@ import 'package:halkyon_learn_flutter/src/feature/study/widget/rating_row.dart';
|
|||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
import '../../support/fake_card_gateway.dart';
|
||||
import '../../support/fake_studyset_gateway.dart';
|
||||
import '../../support/memory_device_settings_store.dart';
|
||||
import '../../support/pump_app.dart';
|
||||
import '../../support/test_dependencies.dart';
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue