feat(engine): 五笔前缀逐键提示与调频接线

- 码表查询从精确匹配改为前缀匹配:精确等长行优先(保住一级/二级简码),
  前缀行按权重降序跟随,LIMIT 50 截断(引擎层不读 server 配置的分层边界)
- WubiCandidateProvider 注入 RuntimePaths:接通 update_weight / delete_by_key
  到 user_dictionary journal;新增 bump_wubi_weight(同码组 MAX+1、主库更新
  与 journal upsert 同事务,行缺失不造行)
- create_word / cache_dynamic_candidate 补「为何 no-op」注释(后续任务范畴)
- 两个 toml 的 wubi_schema 注释与实现对齐:当前仅支持 wubi86
- server 测试:前缀语义、LIMIT 截断、上界构造、调频+journal 回放、删词墓碑
This commit is contained in:
Jia Sui committed 2026-09-22 09:23:25 +08:00
1 parent cdfad7c04c
commit c6632e8d85
8 files changed
+301 -29

No files matched your search

+1 -1
View File
@@ -3,7 +3,7 @@
ProviderRegistry::ProviderRegistry(const ShuangpinProfile &shuangpin_profile, metasequoia::RuntimePaths paths)
: pinyin_provider_(shuangpin_profile, paths),
wubi_provider_(metasequoia::path_to_utf8(paths.dictionary(metasequoia::assets::main_dictionary))),
wubi_provider_(metasequoia::path_to_utf8(paths.dictionary(metasequoia::assets::main_dictionary)), paths),
japanese_provider_(metasequoia::path_to_utf8(paths.dictionary(metasequoia::assets::main_dictionary)),
metasequoia::path_to_utf8(paths.resource(metasequoia::assets::japanese_model)))
{
+53 -9
View File
@@ -1,15 +1,24 @@
#include "wubi_candidate_provider.h"
#include "../contracts/assets/assets.h"
#include "../core/data_path.h"
#include "../quanpin/quanpin_query.h"
#include "../user_dictionary/user_dictionary_journal.h"
#include <spdlog/spdlog.h>
#include <utility>
namespace
{
constexpr int kNoMutation = 0;
}
// 与 QuanpinDictionary::ERROR_CODE 同值:调用方(learn_candidate)丢弃返回值,这里只保持
// 「成功 0 / 失败非 0」的既有约定。
constexpr int kMutationFailed = -1;
// 前缀查询截断上限:候选窗每页最多 10 个,50 给固定位置/重排留足余量。1 码前缀在真实
// 词库上可命中数千行,必须截断;provider 在引擎层,不读 server 配置(分层边界)。
constexpr int kWubiQueryRowLimit = 50;
} // namespace
WubiCandidateProvider::WubiCandidateProvider(std::string db_path)
: db_path_(db_path.empty() ? quanpin::get_default_db_path() : std::move(db_path))
WubiCandidateProvider::WubiCandidateProvider(std::string db_path, metasequoia::RuntimePaths paths)
: db_path_(db_path.empty() ? quanpin::get_default_db_path() : std::move(db_path)), paths_(std::move(paths))
{
}
@@ -28,7 +37,12 @@ std::vector<WordItem> WubiCandidateProvider::query(const QueryRequest &request)
sqlite3_reset(query_statement_);
sqlite3_clear_bindings(query_statement_);
if (sqlite3_bind_text(query_statement_, 1, request.normalized_input.c_str(), -1, SQLITE_TRANSIENT) != SQLITE_OK)
// 上界 = 前缀末字节 +1:五笔码字母域是 a–y(z 被方案丢弃,规范化后只剩小写字母),
// 末字节自增不会越过域边界,无需进位处理。
std::string upper_bound = request.normalized_input;
++upper_bound.back();
if (sqlite3_bind_text(query_statement_, 1, request.normalized_input.c_str(), -1, SQLITE_TRANSIENT) != SQLITE_OK ||
sqlite3_bind_text(query_statement_, 2, upper_bound.c_str(), -1, SQLITE_TRANSIENT) != SQLITE_OK)
{
return {};
}
@@ -61,28 +75,47 @@ void WubiCandidateProvider::reset_cache()
int WubiCandidateProvider::create_word(SchemeType, std::string, std::string)
{
// 在线造词是后续任务(PRD Non-goals);设置页词典管理已覆盖手工造词。这里保持 no-op,
// 不伪造一条无法通过词组取码规则校验的插入。
return kNoMutation;
}
int WubiCandidateProvider::update_weight_by_pinyin_and_word(SchemeType, std::string, std::string)
int WubiCandidateProvider::update_weight_by_pinyin_and_word(SchemeType, std::string code, std::string word)
{
// 增量语义对齐全拼侧 build_sql_for_updating_word:新权重 = 同码组当前最高权重 + 1,
// 选中候选升到组内首位;主库更新与 journal upsert 同事务完成,升级回放可重建。
if (!user_dictionary::bump_wubi_weight(db_path_, journal_db_path(), code, word))
{
return kMutationFailed;
}
// 权重参与 ORDER BY,丢弃查询连接让下次查询按新权重重新排序。
reset_cache();
return kNoMutation;
}
int WubiCandidateProvider::delete_by_pinyin_and_word(SchemeType, std::string, std::string)
int WubiCandidateProvider::delete_by_pinyin_and_word(SchemeType, std::string code, std::string word)
{
// user_dictionary 删词本就按 DictionaryKind 泛化,五笔此前只是没有接线。
if (!user_dictionary::delete_dictionary_candidate(db_path_, journal_db_path(),
user_dictionary::DictionaryKind::Wubi, code, word))
{
return kMutationFailed;
}
reset_cache();
return kNoMutation;
}
int WubiCandidateProvider::cache_dynamic_candidate(SchemeType, const std::string &, const std::string &,
CandidateSource)
{
// 五笔候选全部来自码表,没有云/AI 动态候选可缓存;no-op 是终态而非缺口。
return kNoMutation;
}
int WubiCandidateProvider::cache_dynamic_candidate_for_request(const QueryRequest &, const std::string &,
CandidateSource)
{
// 同 cache_dynamic_candidate:码表型方案没有动态候选来源。
return kNoMutation;
}
@@ -100,9 +133,15 @@ bool WubiCandidateProvider::ensure_query_statement()
return false;
}
constexpr const char *query_sql = "SELECT \"key\", \"value\", \"weight\" FROM wubi86 "
"WHERE \"key\" = ?1 ORDER BY \"weight\" DESC, rowid ASC";
if (sqlite3_prepare_v2(db_, query_sql, -1, &query_statement_, nullptr) != SQLITE_OK)
const std::string query_sql =
"SELECT \"key\", \"value\", \"weight\" FROM wubi86 "
"WHERE \"key\" >= ?1 AND \"key\" < ?2 "
// 精确等长行优先(保住一级/二级简码),其余前缀行按权重降序;rowid 做稳定序,与现状一致。
// key >= ?1 AND key < ?2 是范围扫描,受益于 key 上任何索引;LIMIT 只在截断处付出排序代价。
"ORDER BY (\"key\" = ?1) DESC, \"weight\" DESC, \"key\" ASC, rowid ASC "
"LIMIT " +
std::to_string(kWubiQueryRowLimit);
if (sqlite3_prepare_v2(db_, query_sql.c_str(), -1, &query_statement_, nullptr) != SQLITE_OK)
{
(void)0;
close_database();
@@ -111,6 +150,11 @@ bool WubiCandidateProvider::ensure_query_statement()
return true;
}
std::string WubiCandidateProvider::journal_db_path() const
{
return metasequoia::path_to_utf8(paths_.user(metasequoia::assets::user_journal));
}
void WubiCandidateProvider::close_database()
{
if (query_statement_ != nullptr)
+7 -1
View File
@@ -1,13 +1,17 @@
#pragma once
#include "candidate_provider.h"
#include "../core/runtime_paths.h"
#include <sqlite3.h>
#include <string>
class WubiCandidateProvider : public ICandidateProvider
{
public:
explicit WubiCandidateProvider(std::string db_path = {});
// paths 供调频/删词写 user journal 用;db_path 是测试注入口,生产路径由 ProviderRegistry
// 按 paths 解析后传入,两者指向同一份主库。
explicit WubiCandidateProvider(std::string db_path = {},
metasequoia::RuntimePaths paths = metasequoia::RuntimePaths::legacy());
~WubiCandidateProvider() override;
WubiCandidateProvider(const WubiCandidateProvider &) = delete;
@@ -30,8 +34,10 @@ class WubiCandidateProvider : public ICandidateProvider
private:
bool ensure_query_statement();
void close_database();
std::string journal_db_path() const;
std::string db_path_;
metasequoia::RuntimePaths paths_;
sqlite3 *db_ = nullptr;
sqlite3_stmt *query_statement_ = nullptr;
};
@@ -735,6 +735,54 @@ bool delete_english_candidate(const std::string &english_db_path, const std::str
return delete_dictionary_candidate(english_db_path, user_db_path, DictionaryKind::English, entry_key, value);
}
bool bump_wubi_weight(const std::string &main_db_path, const std::string &user_db_path, const std::string &key,
const std::string &value)
{
if (key.empty() || value.empty())
return false;
// Initialize the journal before attaching it; every operation owns its connections.
if (!ensure_user_database(user_db_path))
return false;
auto database = open_database(main_db_path, SQLITE_OPEN_READWRITE);
auto attach = database ? prepare(database.get(), "ATTACH DATABASE ?1 AS candidate_journal") : Stmt{};
if (!attach || !bind_text(attach.get(), 1, user_db_path) || sqlite3_step(attach.get()) != SQLITE_DONE)
return false;
attach.reset();
if (!execute_sql(database.get(), "BEGIN IMMEDIATE"))
return false;
const auto rollback = [&]() {
(void)execute_sql(database.get(), "ROLLBACK");
return false;
};
// 增量语义对齐全拼侧 build_sql_for_updating_word:新权重 = 同码组当前最高权重 + 1,
// 选中候选升到组内首位,而不是固定 +1(同码组有更高权重行时固定 +1 不会改变排序)。
auto current = prepare(database.get(), "SELECT MAX(weight) FROM main.\"wubi86\" WHERE \"key\"=?1");
if (!current || !bind_text(current.get(), 1, key) || sqlite3_step(current.get()) != SQLITE_ROW)
return rollback();
const std::int64_t new_weight = clamp_managed_weight(sqlite3_column_int64(current.get(), 0) + 1);
current.reset();
auto bump = prepare(database.get(), "UPDATE main.\"wubi86\" SET weight=?1 WHERE \"key\"=?2 AND \"value\"=?3");
// changes()==0 表示目标行不存在:与 update_wubi_weight 一致,不造行。
if (!bump || sqlite3_bind_int64(bump.get(), 1, new_weight) != SQLITE_OK || !bind_text(bump.get(), 2, key) ||
!bind_text(bump.get(), 3, value) || sqlite3_step(bump.get()) != SQLITE_DONE ||
sqlite3_changes(database.get()) == 0)
return rollback();
bump.reset();
auto journal =
prepare(database.get(), "INSERT INTO candidate_journal.user_dictionary_operations(dictionary,key,value,"
"operation,weight,display) VALUES(?1,?2,?3,'upsert',?4,'')"
" ON CONFLICT(dictionary,key,value) DO UPDATE SET operation='upsert',"
"weight=excluded.weight,display='',updated_at=unixepoch()");
if (!journal || !bind_text(journal.get(), 1, kind_name(DictionaryKind::Wubi)) ||
!bind_text(journal.get(), 2, key) || !bind_text(journal.get(), 3, value) ||
sqlite3_bind_int64(journal.get(), 4, new_weight) != SQLITE_OK || sqlite3_step(journal.get()) != SQLITE_DONE)
return rollback();
journal.reset();
if (!execute_sql(database.get(), "COMMIT"))
return rollback();
return true;
}
bool learn_entered_english_word(const std::string &english_db_path, const std::string &user_db_path,
const std::string &display, std::int64_t weight)
{
@@ -32,6 +32,12 @@ bool ensure_user_database(const std::string &user_db_path);
bool record_pinyin_upsert_from_database(const std::string &main_db_path, const std::string &key,
const std::string &value,
const std::string &user_db_path = default_user_db_path());
// Raise the selected wubi candidate to the top of its own code group (max weight + 1, same
// semantics as the quanpin side) and persist the new weight plus a journal upsert in one
// attached-database transaction. Existing rows only: a missing (key,value) pair returns false
// without inserting. Row-missing behaviour matches update_wubi_weight.
bool bump_wubi_weight(const std::string &main_db_path, const std::string &user_db_path, const std::string &key,
const std::string &value);
struct ReplayResult
{
+1 -1
View File
@@ -194,7 +194,7 @@ shuangpin_preedit_mode = "shuangpin"
# 双拼方案,可选:"xiaohe", "ziranma", "shoudao", "microsoft"
# shuangpin_schema = "xiaohe"
shuangpin_schema = "xiaohe"
# 五笔方案,可选:"wubi86", "wubi98"
# 五笔方案,当前仅支持 "wubi86";98 版待词库发布后另开任务接线
wubi_schema = "wubi86"
# 日语方案,当前支持:"romaji"
japanese_schema = "romaji"
+1 -1
View File
@@ -194,7 +194,7 @@ shuangpin_preedit_mode = "shuangpin"
# 双拼方案,可选:"xiaohe", "ziranma", "shoudao", "microsoft"
# shuangpin_schema = "xiaohe"
shuangpin_schema = "xiaohe"
# 五笔方案,可选:"wubi86", "wubi98"
# 五笔方案,当前仅支持 "wubi86";98 版待词库发布后另开任务接线
wubi_schema = "wubi86"
# 日语方案,当前支持:"romaji"
japanese_schema = "romaji"
+184 -16
View File
@@ -1,10 +1,14 @@
#include "tests/includes/test_framework.h"
#include "tests/includes/test_utf8_path.h"
#include "engine/contracts/assets/assets.h"
#include "engine/core/runtime_paths.h"
#include "engine/providers/wubi_candidate_provider.h"
#include "engine/schemes/wubi_scheme.h"
#include <filesystem>
#include <sqlite3.h>
#include <stdexcept>
#include <string>
#include <windows.h>
namespace
{
@@ -17,6 +21,54 @@ void InputLetters(WubiScheme &scheme, const std::string &keys)
}
}
// 与 test_user_dictionary_journal.cpp 的同名助手同型:打开临时库跑语句、取标量。
class TestDatabase
{
public:
explicit TestDatabase(const std::filesystem::path &path)
{
REQUIRE_EQ(sqlite3_open(test::Utf8(path).c_str(), &db_), SQLITE_OK);
}
~TestDatabase()
{
if (db_ != nullptr)
sqlite3_close(db_);
}
void exec(const std::string &sql)
{
REQUIRE_EQ(sqlite3_exec(db_, sql.c_str(), nullptr, nullptr, nullptr), SQLITE_OK);
}
std::int64_t scalar_int64(const std::string &sql)
{
sqlite3_stmt *stmt = nullptr;
REQUIRE_EQ(sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr), SQLITE_OK);
REQUIRE_EQ(sqlite3_step(stmt), SQLITE_ROW);
const std::int64_t value = sqlite3_column_int64(stmt, 0);
sqlite3_finalize(stmt);
return value;
}
std::int64_t scalar_int(const std::string &sql)
{
return scalar_int64(sql);
}
private:
sqlite3 *db_ = nullptr;
};
// journal 路径经 RuntimePaths 注入临时目录,避免测试写真实用户目录;目录须先存在,
// 与生产环境一致(RuntimePaths 管理的世代目录总是已建好)。
metasequoia::RuntimePaths PathsIn(const std::filesystem::path &directory)
{
metasequoia::RuntimePaths paths;
paths.resources = directory / "resources";
paths.user_data = directory / "user";
paths.cache = directory / "cache";
paths.dictionaries = directory / "resources";
std::filesystem::create_directories(paths.user_data);
return paths;
}
std::filesystem::path CreateWubiDatabase()
{
const auto path = std::filesystem::temp_directory_path() / "msime-wubi-provider-test.db";
@@ -31,7 +83,11 @@ std::filesystem::path CreateWubiDatabase()
const char *sql = "CREATE TABLE wubi86 (\"key\" TEXT, \"value\" TEXT, \"weight\" INTEGER);"
"INSERT INTO wubi86 VALUES ('a', '工', 20);"
"INSERT INTO wubi86 VALUES ('a', '戈', 10);"
"INSERT INTO wubi86 VALUES ('aa', '式', 20);";
"INSERT INTO wubi86 VALUES ('aa', '式', 20);"
// 上界邻居:'y' 前缀查询必须命中 'yh',且排除上界之外的 'z' 行。
"INSERT INTO wubi86 VALUES ('y', '也', 5);"
"INSERT INTO wubi86 VALUES ('yh', '又', 6);"
"INSERT INTO wubi86 VALUES ('z', '越过上界', 999);";
const int result = sqlite3_exec(db, sql, nullptr, nullptr, nullptr);
sqlite3_close(db);
if (result != SQLITE_OK)
@@ -41,6 +97,16 @@ std::filesystem::path CreateWubiDatabase()
}
return path;
}
QueryRequest WubiRequest(const std::string &code)
{
QueryRequest request;
request.scheme = SchemeType::Wubi;
request.raw_input = code;
request.normalized_input = code;
request.valid = true;
return request;
}
} // namespace
TEST_CASE(WubiSchemeBuildsDirectCodeRequest)
@@ -66,27 +132,129 @@ TEST_CASE(WubiSchemeAcceptsAtMostFourCodesAndDoesNotUseZ)
REQUIRE_EQ(scheme.build_request().raw_input, std::string("abc"));
}
TEST_CASE(WubiProviderUsesExactCodeAndFixedWeightOrder)
TEST_CASE(WubiProviderReturnsPrefixMatchesWithExactCodeFirst)
{
const auto db_path = CreateWubiDatabase();
{
WubiCandidateProvider provider(test::Utf8(db_path));
QueryRequest request;
request.scheme = SchemeType::Wubi;
request.raw_input = "a";
request.normalized_input = "a";
request.valid = true;
const auto candidates = provider.query(request);
REQUIRE_EQ(candidates.size(), static_cast<size_t>(2));
REQUIRE_EQ(candidates[0].pinyin, std::string("a"));
REQUIRE_EQ(candidates[0].word, std::string("工"));
REQUIRE_EQ(candidates[0].weight, 20);
REQUIRE_EQ(candidates[1].word, std::string("戈"));
// 1 码:精确等长行在前(按权重),更长前缀行按权重跟随——逐键提示的基础。
const auto one_code = provider.query(WubiRequest("a"));
REQUIRE_EQ(one_code.size(), static_cast<size_t>(3));
REQUIRE_EQ(one_code[0].pinyin, std::string("a"));
REQUIRE_EQ(one_code[0].word, std::string("工"));
REQUIRE_EQ(one_code[0].weight, 20);
REQUIRE_EQ(one_code[1].word, std::string("戈"));
REQUIRE_EQ(one_code[2].pinyin, std::string("aa"));
REQUIRE_EQ(one_code[2].word, std::string("式"));
REQUIRE_EQ(provider.update_weight_by_pinyin_and_word(SchemeType::Wubi, "a", "戈"), 0);
const auto unchanged = provider.query(request);
REQUIRE_EQ(unchanged[0].word, std::string("工"));
// 每一码都是前缀查询:2 码精确命中只剩等长行。
const auto two_code = provider.query(WubiRequest("aa"));
REQUIRE_EQ(two_code.size(), static_cast<size_t>(1));
REQUIRE_EQ(two_code[0].word, std::string("式"));
// 精确优先压过权重:'y'(5) 排在前缀行 'yh'(6) 之前;上界构造排除 'z' 行。
const auto upper_edge = provider.query(WubiRequest("y"));
REQUIRE_EQ(upper_edge.size(), static_cast<size_t>(2));
REQUIRE_EQ(upper_edge[0].word, std::string("也"));
REQUIRE_EQ(upper_edge[1].word, std::string("又"));
}
std::filesystem::remove(db_path);
}
TEST_CASE(WubiProviderLimitsPrefixScanToBoundedRows)
{
const auto directory =
std::filesystem::temp_directory_path() / ("msime-wubi-provider-limit-" + std::to_string(GetCurrentProcessId()));
std::filesystem::create_directories(directory);
const auto db_path = directory / "limit.db";
{
TestDatabase db(db_path);
std::string sql = "CREATE TABLE wubi86(\"key\" TEXT,\"value\" TEXT,\"weight\" INTEGER);";
// 55 个精确行 + 5 个前缀行:LIMIT 截断后可见的只能是权重最高的 50 个精确行,
// 前缀行整体排在精确组之后,不应被返回。
for (int weight = 1; weight <= 55; ++weight)
{
sql += "INSERT INTO wubi86 VALUES('a','字" + std::to_string(weight) + "'," + std::to_string(weight) + ");";
}
for (int weight = 1; weight <= 5; ++weight)
{
sql += "INSERT INTO wubi86 VALUES('ab','前" + std::to_string(weight) + "'," + std::to_string(weight) + ");";
}
db.exec(sql);
}
{
WubiCandidateProvider provider(test::Utf8(db_path));
const auto candidates = provider.query(WubiRequest("a"));
REQUIRE_EQ(candidates.size(), static_cast<size_t>(50));
REQUIRE_EQ(candidates[0].weight, 55);
REQUIRE_EQ(candidates[49].weight, 6);
}
std::filesystem::remove_all(directory);
}
TEST_CASE(WubiProviderLearnsSelectionIntoWeightAndJournal)
{
const auto directory =
std::filesystem::temp_directory_path() / ("msime-wubi-provider-learn-" + std::to_string(GetCurrentProcessId()));
std::filesystem::create_directories(directory);
const auto db_path = directory / "main.db";
const auto paths = PathsIn(directory);
{
TestDatabase db(db_path);
db.exec("CREATE TABLE wubi86(\"key\" TEXT,\"value\" TEXT,\"weight\" INTEGER);"
"INSERT INTO wubi86 VALUES('aa','式',20);"
"INSERT INTO wubi86 VALUES('aa','工',10);");
}
{
WubiCandidateProvider provider(test::Utf8(db_path), paths);
// 选中次位候选:权重升到同码组最高(MAX+1),与全拼 build_sql_for_updating_word 语义一致。
REQUIRE_EQ(provider.update_weight_by_pinyin_and_word(SchemeType::Wubi, "aa", "工"), 0);
const auto candidates = provider.query(WubiRequest("aa"));
REQUIRE_EQ(candidates.size(), static_cast<size_t>(2));
REQUIRE_EQ(candidates[0].word, std::string("工"));
REQUIRE_EQ(candidates[0].weight, 21);
// 行不存在不造行。
REQUIRE_EQ(provider.update_weight_by_pinyin_and_word(SchemeType::Wubi, "ab", "不存在"), -1);
}
{
TestDatabase journal(paths.user(metasequoia::assets::user_journal));
REQUIRE_EQ(journal.scalar_int("SELECT COUNT(*) FROM user_dictionary_operations WHERE dictionary='wubi' "
"AND key='aa' AND value='工' AND operation='upsert' AND weight=21"),
1);
REQUIRE_EQ(journal.scalar_int("SELECT COUNT(*) FROM user_dictionary_operations WHERE value='不存在'"), 0);
}
std::filesystem::remove_all(directory);
}
TEST_CASE(WubiProviderDeletesWubiCandidateThroughJournal)
{
const auto directory = std::filesystem::temp_directory_path() /
("msime-wubi-provider-delete-" + std::to_string(GetCurrentProcessId()));
std::filesystem::create_directories(directory);
const auto db_path = directory / "main.db";
const auto paths = PathsIn(directory);
{
TestDatabase db(db_path);
db.exec("CREATE TABLE wubi86(\"key\" TEXT,\"value\" TEXT,\"weight\" INTEGER);"
"INSERT INTO wubi86 VALUES('aa','式',20);"
"INSERT INTO wubi86 VALUES('aa','工',10);");
}
{
WubiCandidateProvider provider(test::Utf8(db_path), paths);
REQUIRE_EQ(provider.delete_by_pinyin_and_word(SchemeType::Wubi, "aa", "式"), 0);
// 再删已不存在的行:changes()==0,失败且不写墓碑。
REQUIRE_EQ(provider.delete_by_pinyin_and_word(SchemeType::Wubi, "aa", "式"), -1);
const auto remaining = provider.query(WubiRequest("aa"));
REQUIRE_EQ(remaining.size(), static_cast<size_t>(1));
REQUIRE_EQ(remaining[0].word, std::string("工"));
}
{
TestDatabase journal(paths.user(metasequoia::assets::user_journal));
REQUIRE_EQ(journal.scalar_int("SELECT COUNT(*) FROM user_dictionary_operations WHERE dictionary='wubi' "
"AND key='aa' AND value='式' AND operation='delete'"),
1);
}
std::filesystem::remove_all(directory);
}