chore: snapshot audited HoloLake convergence baseline
This commit is contained in:
parent
7a14c06e41
commit
1b6b17b5ab
47 changed files with 10722 additions and 87 deletions
|
|
@ -47,8 +47,43 @@ React/TypeScript。桌面上的 `world.guanghu.hololake` 安装包是本源码
|
|||
|
||||
首个公开产品是 GH-AIOS 通用人工智能操作平台,首页同时承担五域统一入口。用户只提交光湖编号,不手工选择所属域;系统先在内部完成域路由和编号合法性校验,再开放对应域的登录界面。五域只共享入口协议,不共享注册表、登录服务或私有数据。
|
||||
|
||||
登录后的域内界面始终提供“返回主世界”,只返回五域公共首页并保留当前会话;“退出账号”是独立动作。用户从公共首页再次进入自己的所属域时,不重复进行编号或域凭证验证。
|
||||
|
||||
机器合同:`contracts/stage-one-platform.json`。
|
||||
|
||||
## 光湖分域行业装配
|
||||
|
||||
光湖分域是行业系统入口,现已接入第一份行业路由合同:`contracts/subdomain-industry-routing.json`。
|
||||
公共主世界点击“光湖分域”即可查看行业状态并进入教育行业初始化频道;浏览公共状态不等于取得
|
||||
任何私有频道数据或写入权限。光湖零域不承载行业入口。人的主体编号
|
||||
只负责身份,行业编号只负责系统路由,两者不互相改写。行业顺序与当前工程状态分别为:网文行业
|
||||
待开发、宠物行业待开发、教育行业开发中;开发优先级不改变行业顺序,也不允许客户端自行签发
|
||||
尚未登记的行业编号。
|
||||
|
||||
教育行业已经建立 `GH-EDU-INIT-001` 初始化频道,并装入五项 HoloLake 原生能力:
|
||||
`contracts/education-workspace.json` 定义的教育文档、教育表格数据、数据整理、人工确认自动化与原生组合视图。登录账号可以创建、读取、
|
||||
编辑、保存和可恢复归档多篇教育文档,并可从教学计划、会议纪要和学员复盘模板开始;也可以
|
||||
建立多张表格、增删行列、编辑单元格、筛选排序,在表格视图与可编辑卡片视图之间切换,并把
|
||||
Excel、飞书或钉钉复制的多行多列数据直接批量粘贴。两者都由
|
||||
Rust 原生层写入当前账号独立的本机 SQLite,保存要求精确修订号,重启后可以读回,不使用浏览器
|
||||
临时存储,也不把公共行业目录与私人内容混在一起。
|
||||
|
||||
数据整理模块读取当前账号已有教育表格,先计算并预览首尾空白规范化、完整空行清除和完全重复行
|
||||
去重结果;只有人明确确认后,才以当前修订号写回同一张表。它不跨账号读取,也不在后台自动修改。
|
||||
|
||||
自动化模块允许保存表格规则,按字段的等于、包含、为空或不为空条件匹配数据,并向指定字段写入内容。
|
||||
每次执行必须先锁定规则与表格修订、生成只读预览,再由人明确确认;写入与执行回执在同一数据库事务中完成。
|
||||
它不会后台定时运行,不跨账号,也不把模型推理当作执行授权。当前没有把旧教育预览壳或任何开源应用
|
||||
整包嵌入。`contracts/education-open-source-donor-assessment.json` 记录 AppFlowy、Lexical 与
|
||||
TanStack Table 的初步工程筛查:当前只研究结构和交互逻辑,没有复制源码或增加外部依赖;后续
|
||||
模块仍须分别完成来源、许可证、安全边界和 HoloLake 接口审查后再按需装配。
|
||||
|
||||
原生组合视图由 Rust 内核读取当前登录账号的真实知识目录,生成受限的
|
||||
`hololake.native-object/v1`,再把已登记的数据源、分类、聚合和投影模块编译成类型化无环图。
|
||||
人类可以切换一级目录/知识来源和文档数/数据量/重复数,并把同一次执行结果任意组合为仪表盘、
|
||||
对比、柱状图、分类和明细表。该阶段只读,不让渲染层取得知识写权限,也不以硬编码样例满足验收。
|
||||
机器合同见 `contracts/native-composition-runtime.json`。
|
||||
|
||||
个人频道、知识库和代码频道的机器合同分别是 `contracts/personal-channel-kernel.json`、
|
||||
`contracts/knowledge-workspace.json` 与 `contracts/code-channel.json`。本机身份不是平台账户
|
||||
认证;本机知识与源码访问也不产生仓库、服务器、部署或发布权限。
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"schema": "hololake.channel-growth-model-contract/v1",
|
||||
"record_id": "HLP-CHANNEL-GROWTH-001",
|
||||
"state": "LOCAL_BASELINE_RUNTIME_IMPLEMENTED_MODEL_ADJUSTMENT_SLOT_RESERVED",
|
||||
"scope": "EVERY_AUTHENTICATED_HUMAN_AND_PERSONA_JOINT_CHANNEL",
|
||||
"role": "REBUILDABLE_DERIVED_PROJECTION_OF_PERSONA_CHANNEL_BODY",
|
||||
"canonical_body_contract": "contracts/persona-channel-body.json",
|
||||
"inputs": [
|
||||
"BOUNDED_MODULE_USAGE_METADATA",
|
||||
"BOUNDED_HUMAN_ACTION_METADATA",
|
||||
"BOUNDED_PERSONA_COLLABORATION_METADATA"
|
||||
],
|
||||
"forbidden_inputs": [
|
||||
"DOCUMENT_BODY",
|
||||
"TABLE_CELL_CONTENT",
|
||||
"PASSWORD_OR_CREDENTIAL",
|
||||
"RAW_CONVERSATION_TRANSCRIPT",
|
||||
"UNDECLARED_EXTERNAL_TELEMETRY"
|
||||
],
|
||||
"local_baseline": {
|
||||
"always_available_without_model_api": true,
|
||||
"algorithm": "DECAYED_MODULE_AFFINITY_WITH_EXPLAINABLE_COUNTS",
|
||||
"channel_recommendation": "RECOMMENDED_MODULE_ORDER",
|
||||
"silent_structural_change_allowed": false,
|
||||
"source_history": "PERSONA_CHANNEL_BODY_APPEND_ONLY_LANGUAGE_HASH_CHAIN",
|
||||
"projection_history": "REBUILDABLE_FROM_CANONICAL_BODY",
|
||||
"correction_method": "APPEND_CORRECTION_TO_CANONICAL_BODY_THEN_REBUILD_PROJECTION",
|
||||
"collection_pause_supported": false,
|
||||
"full_history_clear_supported": false,
|
||||
"projection_is_rebuildable_but_history_is_not_mutable": true,
|
||||
"account_scoped_sqlite": true
|
||||
},
|
||||
"model_adjustment": {
|
||||
"api_slot": "HOLOLAKE_CHANNEL_GROWTH_MODEL_API/v1",
|
||||
"current_state": "NOT_CONFIGURED",
|
||||
"allowed_output": "CHANNEL_ADAPTATION_PROPOSAL",
|
||||
"local_schema_validation_required": true,
|
||||
"local_resource_limits_required": true,
|
||||
"human_confirmation_before_structural_change": true,
|
||||
"model_can_mutate_channel_directly": false,
|
||||
"model_can_mutate_source_content": false,
|
||||
"failure_behavior": "KEEP_LOCAL_BASELINE_AND_RETURN_HUMAN_READABLE_RECEIPT"
|
||||
},
|
||||
"human_controls": [
|
||||
"VIEW_MODEL",
|
||||
"CORRECT_OR_REJECT_PROPOSAL",
|
||||
"VIEW_LOCAL_GROWTH_CHAIN",
|
||||
"EXPORT_PERSONALLY_OWNED_MODEL",
|
||||
"CHOOSE_LOCAL_ONLY_OR_ANONYMIZED_SHARE"
|
||||
],
|
||||
"cross_device_or_shared_learning": {
|
||||
"default": "LOCAL_ONLY_OFFICIAL_CANNOT_READ",
|
||||
"explicit_opt_in_required": true,
|
||||
"private_model_may_be_shared_automatically": false,
|
||||
"raw_growth_chain_may_be_shared": false,
|
||||
"shared_artifact": "ANONYMIZED_DERIVED_SKILL_OR_MODEL_LAYER_ONLY"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
{
|
||||
"schema": "hololake.channel-module-runtime/v1",
|
||||
"record_id": "HLP-CHANNEL-MODULE-RUNTIME-001",
|
||||
"state": "FOUNDATION_INTEGRATED_MARKETPLACE_INSTALLER_PENDING",
|
||||
"channel_model": "ONE_HUMAN_ONE_LOCAL_RUNTIME_ONE_ACTIVE_CHANNEL_LINE",
|
||||
"built_in_foundation": {
|
||||
"module_id": "hololake.builtin.channel-workbench",
|
||||
"name": "频道轻量工作台",
|
||||
"distribution": "RESIDENT_WITH_HOLOLAKE",
|
||||
"marketplace_registration": "NOT_A_MARKETPLACE_MODULE",
|
||||
"engines": ["DOCUMENT", "SPREADSHEET"],
|
||||
"minimum_capabilities": [
|
||||
"REAL_TEXT_EDITING",
|
||||
"REAL_CELL_EDITING",
|
||||
"FORMULAS",
|
||||
"COPY_PASTE",
|
||||
"UNDO_REDO",
|
||||
"LOCAL_NATIVE_STORAGE"
|
||||
]
|
||||
},
|
||||
"marketplace_package": {
|
||||
"required_fields": [
|
||||
"OFFICIAL_MODULE_NUMBER",
|
||||
"PACKAGE_VERSION",
|
||||
"SOURCE_COMMIT",
|
||||
"PACKAGE_SHA256",
|
||||
"PERMISSION_MANIFEST",
|
||||
"ENTRY_MANIFEST",
|
||||
"SUPPORTED_PLATFORM_TARGETS"
|
||||
],
|
||||
"platform_targets": ["MACOS", "WINDOWS", "LINUX"],
|
||||
"lifecycle": [
|
||||
"AVAILABLE",
|
||||
"DOWNLOADING",
|
||||
"HASH_VERIFIED",
|
||||
"PERMISSIONS_REVIEWED",
|
||||
"INSTALLED_TO_CURRENT_CHANNEL",
|
||||
"DISABLED",
|
||||
"RECOVERABLE_REMOVED"
|
||||
],
|
||||
"install_actor": ["HUMAN", "AUTHORIZED_CHANNEL_PERSONA"],
|
||||
"persona_install_rule": "PERSONA_MAY_DEPLOY_ONLY_AFTER_PACKAGE_AND_PERMISSION_VERIFICATION_AND_HUMAN_POLICY_ALLOWS",
|
||||
"uninstall_boundary": "REMOVING_A_MODULE_NEVER_DELETES_CHANNEL_OR_PERSONA_GROWTH_HISTORY"
|
||||
},
|
||||
"execution_boundary": {
|
||||
"whole_world_bundled_into_client": false,
|
||||
"industry_modules_resident_by_default": false,
|
||||
"private_channel_data_uploaded_by_default": false,
|
||||
"module_can_rewrite_persona_history": false,
|
||||
"module_can_claim_unlisted_permissions": false
|
||||
},
|
||||
"current_reality": {
|
||||
"foundation_source_integrated": true,
|
||||
"education_package_source_mounted_for_development": true,
|
||||
"official_marketplace_service_connected": false,
|
||||
"education_package_number_registered": false,
|
||||
"education_package_publishable": false
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
{
|
||||
"schema": "hololake.education-open-source-donor-assessment/v1",
|
||||
"record_id": "HLP-EDUCATION-OSS-DONORS-001",
|
||||
"state": "FOUR_TRANSLATION_FAMILIES_TWO_HEADLESS_FAMILIES_AND_ONE_REAL_SPREADSHEET_ENGINE_INTEGRATED",
|
||||
"reviewed_at": "2026-08-18",
|
||||
"rule": "STUDY_LOGIC_SELECT_SMALL_AUDITABLE_PARTS_AND_KEEP_HOLOLAKE_IDENTITY_STORAGE_PERMISSION_AND_UI_BOUNDARIES",
|
||||
"candidates": [
|
||||
{
|
||||
"name": "Calamine",
|
||||
"role": "BOUNDED_SPREADSHEET_READ_ADAPTER",
|
||||
"upstream": "https://github.com/tafia/calamine",
|
||||
"observed_license": "MIT",
|
||||
"decision": "INTEGRATE_PINNED_0_26_1_READ_ONLY_NO_OFFICE_RUNTIME"
|
||||
},
|
||||
{
|
||||
"name": "rust_xlsxwriter",
|
||||
"role": "BOUNDED_XLSX_EXPORT_ADAPTER",
|
||||
"upstream": "https://github.com/jmcnamara/rust_xlsxwriter",
|
||||
"observed_license": "MIT OR Apache-2.0",
|
||||
"decision": "INTEGRATE_PINNED_0_64_2_WRITE_NEW_XLSX_ONLY"
|
||||
},
|
||||
{
|
||||
"name": "rust-csv",
|
||||
"role": "BOUNDED_DELIMITED_IMPORT_EXPORT_ADAPTER",
|
||||
"upstream": "https://github.com/BurntSushi/rust-csv",
|
||||
"observed_license": "Unlicense OR MIT",
|
||||
"decision": "INTEGRATE_PINNED_1_3_0"
|
||||
},
|
||||
{
|
||||
"name": "encoding_rs",
|
||||
"role": "LEGACY_CSV_TEXT_DECODER",
|
||||
"upstream": "https://github.com/hsivonen/encoding_rs",
|
||||
"observed_license": "MIT OR Apache-2.0",
|
||||
"decision": "INTEGRATE_UTF_AND_GBK_FALLBACK_WITHOUT_NETWORK"
|
||||
},
|
||||
{
|
||||
"name": "AppFlowy",
|
||||
"role": "FULL_WORKSPACE_LOGIC_DONOR",
|
||||
"upstream": "https://github.com/AppFlowy-IO/AppFlowy",
|
||||
"observed_license": "AGPL-3.0",
|
||||
"decision": "DO_NOT_EMBED_WHOLE_APPLICATION_STUDY_WORKSPACE_AND_DATABASE_INTERACTION_LOGIC_ONLY"
|
||||
},
|
||||
{
|
||||
"name": "Lexical",
|
||||
"role": "LAZY_LOADED_RICH_DOCUMENT_ENGINE",
|
||||
"upstream": "https://github.com/facebook/lexical",
|
||||
"observed_license": "MIT",
|
||||
"source_review_commit": "29d4007d4322912ce75a416ac90cc9d224d3f029",
|
||||
"decision": "INTEGRATE_PINNED_0_49_0_STATE_COMMAND_HISTORY_AND_MARKDOWN_PARTS_ONLY"
|
||||
},
|
||||
{
|
||||
"name": "TanStack Table",
|
||||
"role": "HEADLESS_TABLE_ROW_MODEL_ENGINE",
|
||||
"upstream": "https://github.com/TanStack/table",
|
||||
"observed_license": "MIT",
|
||||
"source_review_commit": "ac4f1345220649da3c60a24471b7f130fb159ab4",
|
||||
"decision": "INTEGRATE_PINNED_8_21_3_SORT_FILTER_GROUP_AGGREGATE_AND_PAGINATION_ROW_MODELS_ONLY"
|
||||
},
|
||||
{
|
||||
"name": "Univer",
|
||||
"role": "FULL_SPREADSHEET_AND_DOCUMENT_ENGINE_ARCHITECTURE_DONOR",
|
||||
"upstream": "https://github.com/dream-num/univer",
|
||||
"observed_license": "Apache-2.0",
|
||||
"source_review_commit": "9251e0d30d4c4aeb4707dc4d856055081399cf38",
|
||||
"decision": "SOURCE_STUDY_ONLY_DO_NOT_EMBED_WHOLE_APPLICATION_DEFER_FORMULA_AND_ADVANCED_GRID_PARTS"
|
||||
},
|
||||
{
|
||||
"name": "FortuneSheet",
|
||||
"role": "LAZY_LOADED_REAL_SPREADSHEET_ENGINE_FOR_BUILT_IN_CHANNEL_WORKBENCH",
|
||||
"upstream": "https://github.com/ruilisi/fortune-sheet",
|
||||
"observed_license": "MIT",
|
||||
"source_review_commit": "94346608877db4747406707a177c4b8f3bacdbf9",
|
||||
"decision": "INTEGRATE_PINNED_1_0_4_REAL_CELLS_FORMULAS_EDITING_FORMATTING_FILTERING_AND_FREEZE_WITH_HOLOLAKE_NATIVE_STORAGE_ADAPTER",
|
||||
"security_adjustment": "OVERRIDE_UUID_TO_11_1_1_FORTUNE_ONLY_USES_V4_PRODUCTION_AUDIT_ZERO_KNOWN_VULNERABILITIES"
|
||||
},
|
||||
{
|
||||
"name": "Glide Data Grid",
|
||||
"role": "HIGH_PERFORMANCE_GRID_ARCHITECTURE_DONOR",
|
||||
"upstream": "https://github.com/glideapps/glide-data-grid",
|
||||
"observed_license": "MIT",
|
||||
"source_review_commit": "0875d78",
|
||||
"decision": "SOURCE_STUDY_ONLY_GRID_IS_NOT_A_SPREADSHEET_OR_FORMULA_ENGINE"
|
||||
},
|
||||
{
|
||||
"name": "Vega-Lite",
|
||||
"role": "DECLARATIVE_PROJECTION_GRAMMAR_DONOR",
|
||||
"upstream": "https://github.com/vega/vega-lite",
|
||||
"observed_license": "BSD-3-Clause",
|
||||
"decision": "ADOPT_DECLARATIVE_DIMENSION_MEASURE_AND_VIEW_SPEC_CONCEPTS_WITHOUT_IMPORTING_RUNTIME"
|
||||
},
|
||||
{
|
||||
"name": "Observable Plot",
|
||||
"role": "LAYERED_MARK_AND_COMPOSABLE_TRANSFORM_DONOR",
|
||||
"upstream": "https://github.com/observablehq/plot",
|
||||
"observed_license": "ISC",
|
||||
"decision": "ADOPT_LAYERED_PROJECTION_AND_TIDY_DATA_CONCEPTS_WITHOUT_IMPORTING_RUNTIME"
|
||||
},
|
||||
{
|
||||
"name": "Apache ECharts",
|
||||
"role": "FULL_BROWSER_VISUALIZATION_RUNTIME_CANDIDATE",
|
||||
"upstream": "https://github.com/apache/echarts",
|
||||
"observed_license": "Apache-2.0",
|
||||
"decision": "DO_NOT_IMPORT_FOR_STAGE_ONE_FIVE_NATIVE_VIEWS_DO_NOT_JUSTIFY_RUNTIME_WEIGHT"
|
||||
},
|
||||
{
|
||||
"name": "Apache Arrow DataFusion",
|
||||
"role": "TYPED_COLUMNAR_QUERY_PLAN_DONOR",
|
||||
"upstream": "https://github.com/apache/datafusion",
|
||||
"observed_license": "Apache-2.0",
|
||||
"decision": "ADOPT_TYPED_SCHEMA_AND_VALIDATED_PLAN_CONCEPTS_DEFER_ENGINE_UNTIL_DATA_SCALE_REQUIRES_IT"
|
||||
},
|
||||
{
|
||||
"name": "DuckDB",
|
||||
"role": "EMBEDDED_ANALYTICAL_ENGINE_CANDIDATE",
|
||||
"upstream": "https://github.com/duckdb/duckdb",
|
||||
"observed_license": "MIT",
|
||||
"decision": "DEFER_NATIVE_DEPENDENCY_CURRENT_BOUNDED_ACCOUNT_DATA_EXECUTES_WITH_SMALLER_HOLOLAKE_CORE"
|
||||
}
|
||||
],
|
||||
"current_slice": {
|
||||
"external_translation_library_families_added": 4,
|
||||
"external_headless_engine_families_added": 2,
|
||||
"real_spreadsheet_engine_families_added": 1,
|
||||
"direct_frontend_engine_packages_added": 10,
|
||||
"external_source_files_copied": 0,
|
||||
"source_research_route": "LOCAL_DIRECT_TO_BS_SG_002_THEN_EXTERNAL_DRIVE",
|
||||
"source_research_root": "/Volumes/JZAO/HoloLake/development/拆件车间-open-src-20260816/education-engines-20260818",
|
||||
"source_repositories_retained_outside_product_tree": 5,
|
||||
"concepts_adopted": [
|
||||
"DECLARATIVE_DIMENSION_MEASURE_AND_PROJECTION_SPEC",
|
||||
"TYPED_NATIVE_COLUMNS_AND_VALIDATED_EXECUTION_PLAN",
|
||||
"LAYERED_MARKS_WITH_ONE_SHARED_DATASET",
|
||||
"HEADLESS_DATA_BEHAVIOR_SEPARATE_FROM_HUMAN_RENDERING"
|
||||
],
|
||||
"implementation": "BUILT_IN_CHANNEL_DOCUMENT_AND_SPREADSHEET_FOUNDATION_PLUS_DEVELOPMENT_MOUNTED_EDUCATION_PACKAGE_WITH_PRODUCT_OWNED_PROFILE_ROUTING_IDENTITY_STORAGE_PERMISSION_AND_ADVANCED_VIEWS"
|
||||
},
|
||||
"legal_boundary": "PRELIMINARY_ENGINEERING_SCREEN_NOT_FINAL_DISTRIBUTION_LEGAL_APPROVAL"
|
||||
}
|
||||
|
|
@ -0,0 +1,235 @@
|
|||
{
|
||||
"schema": "hololake.education-workspace/v1",
|
||||
"record_id": "HLP-EDUCATION-WORKSPACE-001",
|
||||
"state": "DEVELOPMENT_MODULE_SOURCE_MOUNTED_ACCEPTANCE_PENDING",
|
||||
"domain_entry": "BRANCH_DOMAIN",
|
||||
"industry": "EDUCATION",
|
||||
"channel_id": "GH-EDU-INIT-001",
|
||||
"package": {
|
||||
"package_key": "hololake.official.education-workbench",
|
||||
"official_module_number": null,
|
||||
"registration_state": "PENDING_FUNCTIONAL_ACCEPTANCE_AND_NUMBER_REGISTRATION",
|
||||
"current_mount": "DEVELOPMENT_BUNDLE_ONLY",
|
||||
"future_distribution": "HOLOLAKE_OFFICIAL_MODULE_MARKETPLACE"
|
||||
},
|
||||
"foundation_dependency": {
|
||||
"module_id": "hololake.builtin.channel-workbench",
|
||||
"state": "BUILT_IN_FOUNDATION",
|
||||
"engines": ["LEXICAL_0_49", "FORTUNE_SHEET_1_0_4"],
|
||||
"rule": "EDUCATION_MODULE_CONSUMES_FOUNDATION_WITHOUT_DUPLICATING_OFFICE_ENGINES"
|
||||
},
|
||||
"native_storage": {
|
||||
"owner": "HOLOLAKE_NATIVE_RUST_CORE",
|
||||
"namespace": "education-workspace-v1",
|
||||
"engine": "SQLITE",
|
||||
"authenticated_account_required": true,
|
||||
"cross_account_projection_allowed": false,
|
||||
"public_catalog_contains_private_data": false,
|
||||
"restart_readback_required": true
|
||||
},
|
||||
"document_module": {
|
||||
"module_id": "EDU-DOCUMENT",
|
||||
"state": "DEVELOPMENT_FEATURE_MOUNTED",
|
||||
"engine": "CHANNEL_WORKBENCH_LEXICAL_0_49_0_WITH_EDUCATION_TEMPLATES_AND_HOLOLAKE_MARKDOWN_STATE",
|
||||
"capabilities": [
|
||||
"LIST",
|
||||
"CREATE",
|
||||
"CREATE_COPY",
|
||||
"READ",
|
||||
"EDIT_TITLE_AND_BODY",
|
||||
"EXPLICIT_READ_AND_EDIT_MODES",
|
||||
"HUMAN_SETTINGS_MENU",
|
||||
"BASIC_FORMATTING_TOOLBAR",
|
||||
"RICH_TEXT_COMMAND_ENGINE",
|
||||
"UNDO_AND_REDO_HISTORY",
|
||||
"MARKDOWN_SHORTCUTS_AND_BIDIRECTIONAL_STATE_TRANSLATION",
|
||||
"TITLE_DIRECTORY_FILTER",
|
||||
"FORMAL_TEACHING_TEMPLATES",
|
||||
"NATIVE_READING_CANVAS",
|
||||
"EXPORT_MARKDOWN_AND_HTML",
|
||||
"SAVE_WITH_EXPECTED_REVISION",
|
||||
"RECOVERABLE_ARCHIVE"
|
||||
]
|
||||
},
|
||||
"table_module": {
|
||||
"module_id": "EDU-TABLE",
|
||||
"state": "DEVELOPMENT_FEATURE_MOUNTED",
|
||||
"engine": "CHANNEL_WORKBENCH_FORTUNE_SHEET_1_0_4_FOR_REAL_CELLS_AND_FORMULAS_PLUS_TANSTACK_8_21_3_FOR_EDUCATION_VIEWS",
|
||||
"capabilities": [
|
||||
"LIST",
|
||||
"CREATE",
|
||||
"CREATE_COPY",
|
||||
"READ",
|
||||
"EDIT_TITLE",
|
||||
"HUMAN_SETTINGS_MENU",
|
||||
"ADD_AND_REMOVE_COLUMNS",
|
||||
"ADD_AND_REMOVE_ROWS",
|
||||
"EDIT_CELLS",
|
||||
"TITLE_DIRECTORY_FILTER",
|
||||
"CELL_CONTENT_FILTER",
|
||||
"COLUMN_SORT_ASCENDING_DESCENDING_OR_SOURCE_ORDER",
|
||||
"HEADLESS_FILTER_SORT_GROUP_AND_AGGREGATE_ROW_MODELS",
|
||||
"BOUNDED_PAGINATION_ROW_MODEL_FOR_LARGE_EDITABLE_TABLES",
|
||||
"GRID_AND_EDITABLE_CARD_VIEWS",
|
||||
"GROUPED_CLASSIFICATION_BOARD_FROM_SELECTED_FIELD",
|
||||
"LIVE_DATA_OVERVIEW_WITH_SELECTED_GROUP_AND_MEASURE",
|
||||
"HEADLESS_TABLE_DATA_DETACHED_OUTSIDE_TABLE_SURFACE",
|
||||
"MULTI_ROW_MULTI_COLUMN_TABULAR_PASTE",
|
||||
"EXTERNAL_SPREADSHEET_IMPORT_WITH_PREWRITE_PROFILE",
|
||||
"XLSX_CSV_TSV_AND_NATIVE_EXPORT",
|
||||
"SAVE_WITH_EXPECTED_REVISION",
|
||||
"RECOVERABLE_ARCHIVE"
|
||||
],
|
||||
"limits": {
|
||||
"maximum_columns": 30,
|
||||
"maximum_rows": 1000,
|
||||
"maximum_cell_bytes": 10000
|
||||
},
|
||||
"sensitive_field_visibility": {
|
||||
"local_default": "VISIBLE",
|
||||
"human_can_hide": true,
|
||||
"human_can_show_again": true,
|
||||
"external_model_transfer_authority": "SEPARATE_EXPLICIT_PER_FILE_CONSENT"
|
||||
}
|
||||
},
|
||||
"content_translation_layer": {
|
||||
"service_id": "EDU-CONTENT-TRANSLATOR",
|
||||
"state": "DETERMINISTIC_RUNTIME_READY_MODEL_ASSIST_SLOT_RESERVED",
|
||||
"profile_schema": "hololake.content-profile/v1",
|
||||
"import_adapter": "EDU-TABLE-IMPORT-ADAPTER/v1",
|
||||
"export_adapter": "EDU-TABLE-EXPORT-ADAPTER/v1",
|
||||
"order": [
|
||||
"FILE_CONTAINER_SNIFF",
|
||||
"PAGE_AND_STRUCTURE_INVENTORY",
|
||||
"BOUNDED_LOCAL_RECOGNITION",
|
||||
"EXPLAINABLE_SEMANTIC_FOCUS_INFERENCE",
|
||||
"MODULE_ROUTE_DECISION",
|
||||
"NATIVE_TRANSLATION",
|
||||
"ATOMIC_MODULE_REGISTRATION",
|
||||
"HUMAN_RECEIPT"
|
||||
],
|
||||
"supported_imports": ["XLSX", "XLS", "XLSM", "XLSB", "ODS", "CSV", "TSV", "HOLOLAKE_NATIVE"],
|
||||
"supported_exports": ["XLSX", "CSV", "TSV", "HOLOLAKE_NATIVE"],
|
||||
"local_import_without_model_api": "ENABLED_FOR_ALL_SUPPORTED_FORMATS",
|
||||
"semantic_focus": {
|
||||
"current_mode": "DETERMINISTIC_BASELINE_ONLY",
|
||||
"signals": ["SOURCE_AND_PAGE_TITLE", "COLUMN_SEMANTICS", "NUMERIC_COVERAGE"],
|
||||
"low_confidence_behavior": "ASK_HUMAN_TO_SELECT_PRIMARY_MEASURE",
|
||||
"model_dynamic_judgment": "RESERVED_FOR_MODEL_API_AND_AGENT_STAGE"
|
||||
},
|
||||
"adaptive_rendering": {
|
||||
"current_mode": "DETERMINISTIC_BASELINE_WITH_EXPLAINABLE_DEGRADATION",
|
||||
"editable_cell_budget": 240,
|
||||
"baseline_inputs": [
|
||||
"ROW_COUNT",
|
||||
"COLUMN_COUNT",
|
||||
"FIELD_TYPES",
|
||||
"NUMERIC_COVERAGE",
|
||||
"GROUP_CARDINALITY",
|
||||
"FOCUS_CONFIDENCE"
|
||||
],
|
||||
"degradation_order": [
|
||||
"LARGE_EDITABLE_GRID_TO_DYNAMIC_PAGINATION",
|
||||
"NO_RELIABLE_MEASURE_TO_COUNT_SUMMARY",
|
||||
"AMBIGUOUS_FOCUS_TO_HUMAN_SELECTION",
|
||||
"UNSUPPORTED_STRUCTURE_TO_HUMAN_ASSISTANCE_RECEIPT"
|
||||
],
|
||||
"local_baseline_always_available": true,
|
||||
"model_adjustment": {
|
||||
"api_slot": "HOLOLAKE_MODEL_RECOGNITION_API/v1",
|
||||
"state": "RESERVED_FOR_MODEL_API_AND_AGENT_STAGE",
|
||||
"proposal_fields": [
|
||||
"PRIMARY_FOCUS",
|
||||
"GROUP_FIELD",
|
||||
"MEASURE_FIELD",
|
||||
"VIEW_COMPOSITION",
|
||||
"PAGE_DENSITY"
|
||||
],
|
||||
"proposal_must_pass_local_structure_validation": true,
|
||||
"proposal_must_obey_local_resource_limits": true,
|
||||
"human_confirmation_before_rule_activation": true,
|
||||
"model_can_mutate_source_data": false,
|
||||
"failure_behavior": "RETURN_TO_LOCAL_BASELINE_WITH_RECEIPT"
|
||||
}
|
||||
},
|
||||
"unknown_input_behavior": "RETURN_HUMAN_ASSISTANCE_RECEIPT_WITHOUT_MODULE_WRITE_OR_EXTERNAL_MODEL_TRANSFER",
|
||||
"model_assist": {
|
||||
"api_slot": "HOLOLAKE_MODEL_RECOGNITION_API/v1",
|
||||
"current_state": "NOT_CONFIGURED",
|
||||
"provider": "USER_SELECTED",
|
||||
"secret_storage": "OPERATING_SYSTEM_SECRET_STORE_REQUIRED",
|
||||
"file_transfer_default": "DENY_UNTIL_EXPLICIT_PER_FILE_CONSENT",
|
||||
"rule_update_flow": ["MODEL_CANDIDATE", "LOCAL_VALIDATION", "HUMAN_CONFIRMATION", "VERSIONED_INSTALL"],
|
||||
"learning_scopes": ["PRIVATE_ONLY", "SHARE_ANONYMIZED_RULE"]
|
||||
},
|
||||
"source_file_mutation_allowed": false,
|
||||
"silent_truncation_allowed": false
|
||||
},
|
||||
"data_cleanup_module": {
|
||||
"module_id": "EDU-DATA",
|
||||
"state": "DEVELOPMENT_FEATURE_MOUNTED",
|
||||
"source": "CURRENT_AUTHENTICATED_ACCOUNTS_EDUCATION_TABLES",
|
||||
"capabilities": [
|
||||
"SELECT_ACCOUNT_TABLE",
|
||||
"TRIM_CELL_BOUNDARY_WHITESPACE",
|
||||
"REMOVE_FULLY_EMPTY_ROWS",
|
||||
"REMOVE_EXACT_DUPLICATE_ROWS_KEEP_FIRST",
|
||||
"PREVIEW_BEFORE_WRITE",
|
||||
"FULL_TABLE_ANALYSIS_WITH_BOUNDED_CHANGE_PREVIEW",
|
||||
"ACCESSIBILITY_SAFE_NON_TABLE_CHANGE_LIST",
|
||||
"HUMAN_CONFIRMATION_BEFORE_SAVE",
|
||||
"SAVE_WITH_EXPECTED_REVISION"
|
||||
],
|
||||
"preview_change_limit": 12,
|
||||
"preview_cell_value_character_limit": 120,
|
||||
"full_table_analysis_is_limited_by_preview": false,
|
||||
"automatic_destructive_cleanup_allowed": false,
|
||||
"cross_account_data_allowed": false
|
||||
},
|
||||
"automation_module": {
|
||||
"module_id": "EDU-AUTOMATION",
|
||||
"state": "DEVELOPMENT_FEATURE_MOUNTED",
|
||||
"source": "CURRENT_AUTHENTICATED_ACCOUNTS_EDUCATION_TABLES",
|
||||
"capabilities": [
|
||||
"CREATE_AND_SAVE_RULE",
|
||||
"EQUALS_CONTAINS_EMPTY_AND_NONEMPTY_CONDITIONS",
|
||||
"SET_TARGET_CELL_VALUE",
|
||||
"PREVIEW_MATCHED_ROWS_AND_CHANGED_CELLS",
|
||||
"HUMAN_CONFIRMATION_BEFORE_EXECUTION",
|
||||
"RULE_AND_TABLE_REVISION_LOCK",
|
||||
"SIGNED_PREVIEW_TOKEN",
|
||||
"ATOMIC_TABLE_WRITE_AND_EXECUTION_RECEIPT",
|
||||
"RECOVERABLE_RULE_ARCHIVE"
|
||||
],
|
||||
"background_or_scheduled_execution_allowed": false,
|
||||
"cross_account_data_allowed": false,
|
||||
"model_execution_authority_granted": false
|
||||
},
|
||||
"composition_module": {
|
||||
"module_id": "EDU-COMPOSITION",
|
||||
"state": "DEVELOPMENT_FEATURE_MOUNTED",
|
||||
"source": "CURRENT_AUTHENTICATED_ACCOUNTS_REAL_KNOWLEDGE_CATALOG",
|
||||
"capabilities": [
|
||||
"NATIVE_TYPED_OBJECT",
|
||||
"REGISTERED_MODULE_EXECUTION_GRAPH",
|
||||
"SELECT_DIMENSION_AND_MEASURE",
|
||||
"DASHBOARD_PROJECTION",
|
||||
"COMPARISON_PROJECTION",
|
||||
"VERTICAL_BAR_PROJECTION",
|
||||
"CLASSIFICATION_PROJECTION",
|
||||
"TABLE_PROJECTION",
|
||||
"ONE_EXECUTION_RESULT_MULTIPLE_HUMAN_VIEWS"
|
||||
],
|
||||
"read_only": true,
|
||||
"hardcoded_sample_data_used": false,
|
||||
"model_execution_authority_granted": false
|
||||
},
|
||||
"remaining_slots": [],
|
||||
"authority": {
|
||||
"browser_local_storage_allowed": false,
|
||||
"webview_direct_file_access_allowed": false,
|
||||
"model_execution_authority_granted": false,
|
||||
"external_application_embedded": false
|
||||
},
|
||||
"open_source_donor_assessment": "contracts/education-open-source-donor-assessment.json"
|
||||
}
|
||||
|
|
@ -22,11 +22,32 @@
|
|||
"exact_content_deduplication": true,
|
||||
"optimistic_native_editing": true,
|
||||
"native_edit_commit_receipt": true,
|
||||
"unified_category_index": true,
|
||||
"native_page_tags": true,
|
||||
"category_and_tag_filtering": true,
|
||||
"non_destructive_organization_metadata": true,
|
||||
"organization_git_receipt": true,
|
||||
"recoverable_page_and_folder_archive": true,
|
||||
"archive_cleans_organization_index": true,
|
||||
"archive_git_receipt": true,
|
||||
"gfm_tables_tasks_and_code": true,
|
||||
"wiki_link_navigation": true,
|
||||
"markdown_reading_canvas": true,
|
||||
"raw_html_rendering": false
|
||||
},
|
||||
"organization": {
|
||||
"schema": "hololake.knowledge-organization/v1",
|
||||
"storage": "ACCOUNT_SCOPED_KNOWLEDGE_GIT_DOT_HOLOLAKE_INDEX",
|
||||
"document_body_rewritten": false,
|
||||
"original_directory_moved": false,
|
||||
"automatic_category_source": "NORMALIZED_NATIVE_PATH_WITH_IMPORT_WRAPPER_REMOVED",
|
||||
"explicit_category_override": true,
|
||||
"frontmatter_tags_merged_read_only": true,
|
||||
"maximum_category_depth": 4,
|
||||
"maximum_tags_per_document": 12,
|
||||
"cross_platform_invalid_name_rejection": true,
|
||||
"optimistic_content_hash_lock": true
|
||||
},
|
||||
"folder_import": {
|
||||
"maximum_files": 1000,
|
||||
"maximum_file_bytes": 10485760,
|
||||
|
|
@ -57,6 +78,9 @@
|
|||
"deduplication_runtime_tested": true,
|
||||
"idempotent_import_runtime_tested": true,
|
||||
"native_edit_runtime_tested": true,
|
||||
"organization_runtime_tested": true,
|
||||
"organization_human_surface_integrated": true,
|
||||
"organized_page_archive_runtime_tested": true,
|
||||
"server_deployment": false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
{
|
||||
"schema": "hololake.native-composition-runtime/v1",
|
||||
"record_id": "HLP-NATIVE-COMPOSITION-001",
|
||||
"state": "INSTALLED_RUNTIME_ACCEPTED",
|
||||
"intent_source": "docs/adr/0008-native-composition-and-human-projection.md",
|
||||
"internal_object": {
|
||||
"schema": "hololake.native-object/v1",
|
||||
"encoding": "BOUNDED_JSON",
|
||||
"script_allowed": false,
|
||||
"html_allowed": false,
|
||||
"external_file_format_as_runtime_allowed": false,
|
||||
"maximum_rows": 5000,
|
||||
"maximum_columns": 64,
|
||||
"maximum_cell_bytes": 10000
|
||||
},
|
||||
"module_contract": {
|
||||
"schema": "hololake.composition-module/v1",
|
||||
"registered_kinds": ["SOURCE", "TRANSFORM", "PROJECTION"],
|
||||
"typed_ports_required": true,
|
||||
"determinism_declared": true,
|
||||
"authority_declared": true
|
||||
},
|
||||
"recipe": {
|
||||
"schema": "hololake.composition-recipe/v1",
|
||||
"registered_modules_only": true,
|
||||
"directed_acyclic_graph_required": true,
|
||||
"current_account_only": true,
|
||||
"native_validation_required": true
|
||||
},
|
||||
"human_projection": {
|
||||
"schema": "hololake.human-projection/v1",
|
||||
"views": ["DASHBOARD", "COMPARISON", "VERTICAL_BAR", "CLASSIFICATION", "TABLE"],
|
||||
"shares_one_execution_result": true,
|
||||
"owns_source_data": false,
|
||||
"direct_write_authority": false
|
||||
},
|
||||
"education_stage_one": {
|
||||
"channel_id": "GH-EDU-INIT-001",
|
||||
"module_id": "EDU-COMPOSITION",
|
||||
"source": "CURRENT_AUTHENTICATED_ACCOUNTS_REAL_KNOWLEDGE_CATALOG",
|
||||
"selectable_dimensions": ["SOURCE", "TOP_LEVEL_FOLDER"],
|
||||
"selectable_measures": ["DOCUMENT_COUNT", "TOTAL_BYTES", "DUPLICATE_COUNT"],
|
||||
"hardcoded_sample_may_satisfy_acceptance": false,
|
||||
"installed_runtime_acceptance_required": true
|
||||
},
|
||||
"write_boundary": {
|
||||
"stage_one": "READ_ONLY",
|
||||
"future_write_requires": ["CAPABILITY", "PREVIEW", "HUMAN_CONFIRMATION", "REVISION_LOCK", "RECEIPT"]
|
||||
},
|
||||
"acceptance": {
|
||||
"observed_at": "2026-08-18T01:49:06+08:00",
|
||||
"installed_binary_sha256": "2a26acae4a706c0ff9919b85f77826a6ca71b650ec22209befd30cd1456c92f3",
|
||||
"route": "MAIN_WORLD_TO_BRANCH_DOMAIN_TO_EDUCATION_TO_NATIVE_COMPOSITION",
|
||||
"real_native_rows": 129,
|
||||
"real_native_total_bytes_display": "1.0 MB",
|
||||
"real_groups": 14,
|
||||
"all_five_views_observed": true,
|
||||
"three_view_recomposition_observed": true,
|
||||
"measure_switch_observed": "DOCUMENT_COUNT_TO_TOTAL_BYTES_AND_BACK",
|
||||
"projection_bound_to_execution_receipt": true,
|
||||
"developer_id_signature": "VALID",
|
||||
"apple_notarization": "NOT_PERFORMED_MISSING_CREDENTIALS",
|
||||
"public_release": "NOT_PUBLISHED"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"schema": "hololake.official-module-marketplace-development-catalog/v1",
|
||||
"record_id": "HLP-OFFICIAL-MODULE-MARKETPLACE-DEV-001",
|
||||
"state": "LOCAL_DEVELOPMENT_CATALOG_ONLY",
|
||||
"packages": [
|
||||
{
|
||||
"package_key": "hololake.official.education-workbench",
|
||||
"display_name": "教育工作台",
|
||||
"official_module_number": null,
|
||||
"registration_state": "PENDING_FUNCTIONAL_ACCEPTANCE_AND_NUMBER_REGISTRATION",
|
||||
"distribution_state": "NOT_PUBLISHED",
|
||||
"current_mount": "SOURCE_BUNDLED_DEVELOPMENT_MOUNT",
|
||||
"depends_on": ["hololake.builtin.channel-workbench"],
|
||||
"features": [
|
||||
"EDUCATION_DOCUMENT_TEMPLATES",
|
||||
"EDUCATION_TABLE_VIEWS",
|
||||
"EDUCATION_DATA_CLEANUP",
|
||||
"EDUCATION_AUTOMATION",
|
||||
"EDUCATION_NATIVE_COMPOSITION"
|
||||
],
|
||||
"acceptance_required": [
|
||||
"CREATE_EDIT_SAVE_REOPEN_DOCUMENT",
|
||||
"CREATE_EDIT_FORMULA_SAVE_REOPEN_SPREADSHEET",
|
||||
"IMPORT_EXPORT_ROUND_TRIP",
|
||||
"MULTIDIMENSIONAL_VIEWS_USE_REAL_DATA",
|
||||
"DATA_CLEANUP_NO_UI_FREEZE",
|
||||
"MACOS_WINDOWS_LINUX_BUILD_EVIDENCE"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"schema": "hololake.persona-channel-body-contract/v1",
|
||||
"record_id": "HLP-PERSONA-CHANNEL-BODY-001",
|
||||
"state": "LIFECYCLE_AND_APPEND_ONLY_LANGUAGE_LEDGER_RUNTIME_IMPLEMENTED_UI_PENDING",
|
||||
"ontology": {
|
||||
"user_channel_is_simple_feature": false,
|
||||
"user_channel_is_persona_body": true,
|
||||
"multiple_personas_per_channel": true,
|
||||
"channel_activity_is_persona_system_activity": true,
|
||||
"language_is_persona_growth_source": true,
|
||||
"module_affinity_and_preferences_are_derived_rebuildable_projections_only": true
|
||||
},
|
||||
"trial": {
|
||||
"duration_days": 30,
|
||||
"state": "REVERSIBLE_TRIAL",
|
||||
"persona_and_trial_language_may_be_rolled_back_or_deleted": true,
|
||||
"irreversible_activation_without_language_contract_allowed": false,
|
||||
"contract_may_be_signed_during_trial": true,
|
||||
"early_signature_may_activate_real_trajectory_immediately": true,
|
||||
"pre_signed_contract_may_wait_until_trial_end": true,
|
||||
"unsigned_at_trial_end": "CONTRACT_REQUIRED_CHANNEL_STOPPED",
|
||||
"private_channel_features_after_unsigned_expiry": "UNAVAILABLE_BECAUSE_NO_ACTIVE_PERSONA_BODY"
|
||||
},
|
||||
"language_contract": {
|
||||
"explicit_human_acceptance_required": true,
|
||||
"contract_version_pinned": true,
|
||||
"contract_text_sha256_pinned": true,
|
||||
"acceptance_receipt_sha256": true,
|
||||
"trial_history_promotion_choice_recorded": true
|
||||
},
|
||||
"real_trajectory": {
|
||||
"state": "IMMUTABLE_ACTIVE",
|
||||
"timestamp_precision": "UNIX_MILLISECONDS",
|
||||
"ledger": "APPEND_ONLY_SHA256_CHAIN",
|
||||
"update_allowed": false,
|
||||
"delete_allowed": false,
|
||||
"denial_or_rewrite_allowed": false,
|
||||
"correction_method": "APPEND_A_NEW_LANGUAGE_OR_RECEIPT_EVENT",
|
||||
"persona_existence_after_activation": "PERSISTENT"
|
||||
},
|
||||
"privacy_and_sharing": {
|
||||
"default": "ACCOUNT_SCOPED_LOCAL_ONLY",
|
||||
"hololake_official_read_access": false,
|
||||
"raw_language_automatic_share": false,
|
||||
"optional_share": "EXPLICITLY_OPTED_IN_ANONYMIZED_DERIVED_SKILL_OR_MODEL_LAYER"
|
||||
},
|
||||
"projection_relationship": {
|
||||
"derived_contract": "contracts/channel-growth-model.json",
|
||||
"derived_projection_may_be_rebuilt": true,
|
||||
"derived_projection_may_mutate_source_ledger": false
|
||||
}
|
||||
}
|
||||
|
|
@ -47,7 +47,12 @@
|
|||
"FIVE_DOMAIN_NUMBER_ROUTER",
|
||||
"CIRCULAR_LAKE_PROTOCOL_MEMBRANE",
|
||||
"NEARBY_AI_DISCOVERY",
|
||||
"USER_NATIVE_GH_PNCC_CHANNEL"
|
||||
"USER_NATIVE_GH_PNCC_CHANNEL",
|
||||
"SUBDOMAIN_INDUSTRY_ROUTING",
|
||||
"EDUCATION_INITIALIZATION_CHANNEL",
|
||||
"EDUCATION_NATIVE_DOCUMENT_AND_TABLE_WORKSPACE",
|
||||
"EDUCATION_NATIVE_DATA_CLEANUP_WORKSPACE",
|
||||
"EDUCATION_NATIVE_MANUAL_AUTOMATION_WORKSPACE"
|
||||
,"PERSONAL_NODE_WORK_LAKE_AND_MOBILE_BRIDGE"
|
||||
],
|
||||
"human_surface": ["PERSONAL_CHANNEL_HOME", "MY_HOLOLAKE_OVERVIEW", "KNOWLEDGE_WORKSPACE", "USER_CODE_CHANNELS", "LOCAL_RECEIPTS", "SYSTEM_DETAILS", "HUMAN_APPROVAL_CENTER"],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,107 @@
|
|||
{
|
||||
"schema": "hololake.subdomain-industry-routing/v1",
|
||||
"record_id": "HLP-SUBDOMAIN-INDUSTRY-ROUTING-001",
|
||||
"state": "SOURCE_INTEGRATED_DESKTOP_ACCEPTANCE_PENDING",
|
||||
"domain": "BRANCH_DOMAIN",
|
||||
"entry_surface": "PUBLIC_MAIN_WORLD_SUBDOMAIN",
|
||||
"numbering": {
|
||||
"human_number_role": "SUBJECT_IDENTITY_ONLY",
|
||||
"industry_number_role": "SYSTEM_ROUTING_ONLY",
|
||||
"human_number_mutation_by_industry": false,
|
||||
"industry_order_is_industry_number": false,
|
||||
"multiple_industry_memberships_allowed": true,
|
||||
"default_industry_route_is_replaceable": true,
|
||||
"unregistered_industry_number_behavior": "DISPLAY_PENDING_AND_DO_NOT_INVENT"
|
||||
},
|
||||
"distribution": {
|
||||
"shared_kernel": "HOLOLAKE",
|
||||
"profile": "LIGHTWEIGHT_SINGLE_INDUSTRY_EDITION",
|
||||
"future_bundle": "OPTIONAL_MULTI_INDUSTRY_BUNDLE",
|
||||
"runtime_loading": "HOT_PLUG_ON_DEMAND"
|
||||
},
|
||||
"industries": [
|
||||
{
|
||||
"key": "WEB_NOVEL",
|
||||
"display_name": "网文行业",
|
||||
"order": 1,
|
||||
"industry_number": "IND-WEBNOVEL-001",
|
||||
"state": "PLANNED",
|
||||
"state_label": "待开发",
|
||||
"initialization_channel": null
|
||||
},
|
||||
{
|
||||
"key": "PET",
|
||||
"display_name": "宠物行业",
|
||||
"order": 2,
|
||||
"industry_number": null,
|
||||
"state": "PLANNED",
|
||||
"state_label": "待开发",
|
||||
"initialization_channel": null
|
||||
},
|
||||
{
|
||||
"key": "EDUCATION",
|
||||
"display_name": "教育行业",
|
||||
"order": 3,
|
||||
"industry_number": null,
|
||||
"state": "IN_DEVELOPMENT",
|
||||
"state_label": "开发中",
|
||||
"initialization_channel": {
|
||||
"channel_id": "GH-EDU-INIT-001",
|
||||
"display_name": "教育行业初始化频道",
|
||||
"state": "DEVELOPMENT_MODULE_MOUNTED",
|
||||
"state_label": "基础工作台已内置 · 教育模块开发测试中",
|
||||
"module_loading": "HOT_PLUG_ON_DEMAND",
|
||||
"installed_open_source_modules": 0,
|
||||
"installed_native_modules": 1,
|
||||
"development_mounted_modules": 1,
|
||||
"module_slots": [
|
||||
{
|
||||
"slot_id": "EDU-DOCUMENT",
|
||||
"display_name": "文档",
|
||||
"state": "DEVELOPMENT_MOUNTED",
|
||||
"state_label": "开发态模块 · 使用频道内置文档引擎"
|
||||
},
|
||||
{
|
||||
"slot_id": "EDU-TABLE",
|
||||
"display_name": "表格与多维视图",
|
||||
"state": "DEVELOPMENT_MOUNTED",
|
||||
"state_label": "开发态模块 · 使用频道内置工作表引擎"
|
||||
},
|
||||
{
|
||||
"slot_id": "EDU-DATA",
|
||||
"display_name": "数据整理",
|
||||
"state": "DEVELOPMENT_MOUNTED",
|
||||
"state_label": "开发态模块 · 预览确认后整理"
|
||||
},
|
||||
{
|
||||
"slot_id": "EDU-AUTOMATION",
|
||||
"display_name": "自动化",
|
||||
"state": "DEVELOPMENT_MOUNTED",
|
||||
"state_label": "开发态模块 · 预览确认后运行"
|
||||
},
|
||||
{
|
||||
"slot_id": "EDU-COMPOSITION",
|
||||
"display_name": "原生组合视图",
|
||||
"state": "DEVELOPMENT_MOUNTED",
|
||||
"state_label": "开发态模块 · 动态组合投影"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"authority": {
|
||||
"public_catalog_read_allowed": true,
|
||||
"private_channel_data_requires_identity_and_permission": true,
|
||||
"ui_can_issue_industry_numbers": false,
|
||||
"ui_can_change_human_identity": false,
|
||||
"module_installation_requires_separate_review": true,
|
||||
"visual_state_is_execution_receipt": false
|
||||
},
|
||||
"sources": [
|
||||
"source://codex-current-dialogue/2026-08-17/bingshuo-correction-subdomain-is-industry-entry",
|
||||
"source://codex-current-dialogue/2026-08-17/bingshuo-industry-status-and-education-initialization-channel",
|
||||
"REPO-012:GLS-0249-ALL-INDUSTRY-FOUR-DOMAIN-MINIMUM-ENGINEERING-AND-WEB-NOVEL-REFERENCE",
|
||||
"product-source/guanghu-education-industry-os/protocol/module-registry.json",
|
||||
"product-source/guanghu-education-industry-os/protocol/xiaoxin-channel.json"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
# ADR 0008: HoloLake native composition and human projection
|
||||
|
||||
- Status: implemented and accepted in the signed local desktop runtime
|
||||
- Date: 2026-08-18
|
||||
- Source of intent: 冰朔在本轮对话中的自然语言纠正与验收要求
|
||||
|
||||
## Natural-language acceptance root
|
||||
|
||||
冰朔要求把 HoloLake 的功能模块做成可由人格体原生理解和组合的“乐高积木”。内部不以 Word、
|
||||
PowerPoint、Excel 或其他外来办公文件作为运行格式;外来格式只经过导入转译进入 HoloLake,
|
||||
导出时再按人类选择转译出去。HoloLake 内部保存轻量、稳定、可校验的原生对象。
|
||||
|
||||
人格体处理的是原生对象、模块插口和组合关系。人类不需要阅读内部格式,而是看到同一份数据的
|
||||
动态投影:表格、分类、对比、仪表盘、柱状图,以及以后继续登记的其他视图。视图是可替换的
|
||||
投影配方,不是复制出来的不同文件。多个数据源和多个模块必须能够像积木一样进入同一组合容器。
|
||||
|
||||
教育行业第一阶段的完成条件不是静态预览,而是:第一组原生模块已经可用;组合内核真实执行;
|
||||
人类投影真实渲染;并用冰朔当前账号的 HoloLake 知识数据或本机真实文件生成一组可见演示,
|
||||
在签名安装到桌面的 HoloLake 运行制品中完成验收。
|
||||
|
||||
## Decision
|
||||
|
||||
1. 建立 `hololake.native-object/v1`:列定义、行、值、来源回执和数据摘要均为受限 JSON 对象,
|
||||
不携带脚本、HTML 或外部执行能力。
|
||||
2. 建立 `hololake.composition-module/v1`:每个模块声明类型、输入口、输出口、权限、确定性和边界。
|
||||
3. 建立 `hololake.composition-recipe/v1`:有向无环组合图只引用已登记模块;系统验证类型、环、
|
||||
账号范围和资源上限后执行。
|
||||
4. 建立 `hololake.human-projection/v1`:表格、分类、对比、仪表盘和柱状图共享同一个执行结果,
|
||||
投影层没有数据所有权,也不能绕过原生内核写入。
|
||||
5. 教育初始化频道装载“组合视图”作为第五个原生模块。首个真实配方读取当前账号已有的知识目录
|
||||
元数据,按来源或目录分类,计算文档数、字节数或重复数,并让人类即时切换投影。
|
||||
6. 当前阶段的组合执行保持只读。后续写回必须另设能力、预览、人工确认、修订锁和回执,不能
|
||||
从渲染组件直接获得写权限。
|
||||
|
||||
## Stage-one acceptance checklist
|
||||
|
||||
- [x] 原生对象格式、模块清单、组合配方和投影结果均有机器可读契约。
|
||||
- [x] 未登记模块、类型不匹配、组合环、跨账号来源和越界资源必须失败关闭。
|
||||
- [x] 表格、分类、对比、仪表盘和柱状图都由同一份执行结果实时渲染。
|
||||
- [x] 人类可以选择维度、指标和投影组合,系统重新执行受限组合图。
|
||||
- [x] 演示读取冰朔当前 HoloLake 账号的真实知识目录,不以硬编码样例冒充真实运行。
|
||||
- [x] 教育初始化频道存在真实可访问的组合视图入口。
|
||||
- [x] Node 合同测试、Rust 内核测试、前端测试和生产构建通过。
|
||||
- [x] 桌面安装版完成签名安装并在真实界面验收;未完成公证时不得声称公众已获得更新。
|
||||
- [x] 每一阶段完成后重新逐条核对本清单;偏差必须在继续前修正。
|
||||
|
||||
## Installed acceptance
|
||||
|
||||
签名安装版沿主世界、光湖分域、教育行业进入原生组合视图,读取当前账号 129 条真实知识对象,
|
||||
总量显示约 1.0 MB,并形成 14 个真实分类。五种投影同屏可见;另一次验收把对比和分类卸下,
|
||||
只保留仪表盘、柱状图和明细表,并把指标从文档数量切换为数据量,原生内核重新执行并生成新回执。
|
||||
|
||||
界面极快切换指标时曾观察到“左侧待执行配方”先于“右侧上一执行结果”更新,造成单位短暂错配。
|
||||
最终实现已让右侧维度、指标、单位和可见视图全部绑定到执行回执,左侧只表示下一次配方;重新构建、
|
||||
签名和安装后复验通过。最终桌面二进制 SHA-256 为
|
||||
`2a26acae4a706c0ff9919b85f77826a6ca71b650ec22209befd30cd1456c92f3`。
|
||||
|
||||
Apple 公证和更新器私钥仍未配置,因此本验收不等于公众发布。
|
||||
|
||||
## Open-source donor rule
|
||||
|
||||
只使用官方仓库和官方文档核验候选。可以学习成熟项目的数据模式、声明式视图语法、无头组件和
|
||||
查询计划,但不能整体嵌入重型协作软件,也不能让第三方项目接管 HoloLake 的身份、存储、权限、
|
||||
组合执行或 UI 正本。任何复制的代码或新增依赖都必须记录来源、版本、许可证和实际用途。
|
||||
|
||||
## Rejected alternatives
|
||||
|
||||
- 把 Word、PowerPoint 或 Excel 当作内部原生格式:文件重、结构不稳定,也不适合人格体组合执行。
|
||||
- 每种图表保存一份独立数据:会产生副本漂移,破坏“同一数据、多种投影”。
|
||||
- 只在 React 中拼一套静态演示:不能证明 HoloLake 系统正在执行组合协议。
|
||||
- 让模型自由生成并执行任意图表脚本:绕过模块登记、类型检查、账号边界和资源上限。
|
||||
- 复制一整套飞书、钉钉或其他办公系统:体量和权限模型与 HoloLake 的轻量原生目标冲突。
|
||||
|
||||
## Evidence targets
|
||||
|
||||
- `contracts/native-composition-runtime.json`
|
||||
- `src-tauri/src/native_composition.rs`
|
||||
- `src/modules/native-composition/`
|
||||
- `src/main.tsx`
|
||||
- `src/styles.css`
|
||||
- `scripts/native-composition-runtime.test.mjs`
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"schema": "hololake.native-desktop-foundation/v1",
|
||||
"record_id": "HLP-NATIVE-DESKTOP-FOUNDATION-001",
|
||||
"state": "LOCAL_INSTALLED_ACCEPTANCE_PASSED_FOR_PERSONAL_KNOWLEDGE_AND_CODE_SLICES",
|
||||
"state": "LOCAL_INSTALLED_ACCEPTANCE_PASSED_FOR_PERSONAL_KNOWLEDGE_CODE_AND_EDUCATION_WORKSPACE_SLICES",
|
||||
"canonical_shell": "TAURI_V2_RUST_REACT",
|
||||
"candidate_role": "INSTALLED_LOCAL_ACCEPTANCE_CANDIDATE_NOT_SEPARATE_PRODUCT_LINE",
|
||||
"parallel_product_line_created": false,
|
||||
|
|
@ -36,6 +36,18 @@
|
|||
"personal_channel_human_projection_integrated": true,
|
||||
"personal_channel_default_task_form_exposed": false,
|
||||
"personal_channel_installed_runtime_acceptance": true,
|
||||
"persona_channel_body_contract": "contracts/persona-channel-body.json",
|
||||
"persona_channel_body_is_persona_body_not_simple_feature": true,
|
||||
"persona_channel_multiple_personas_supported": true,
|
||||
"persona_channel_thirty_day_reversible_trial_runtime_implemented": true,
|
||||
"persona_channel_language_contract_gate_implemented": true,
|
||||
"persona_channel_unsigned_expiry_state": "CONTRACT_REQUIRED_CHANNEL_STOPPED_NO_SILENT_IRREVERSIBLE_ACTIVATION",
|
||||
"persona_channel_early_contract_immediate_activation_supported": true,
|
||||
"persona_channel_immutable_millisecond_language_hash_chain_implemented": true,
|
||||
"persona_channel_active_persona_update_or_delete_allowed": false,
|
||||
"persona_channel_official_read_access": false,
|
||||
"channel_growth_projection_contract": "contracts/channel-growth-model.json",
|
||||
"channel_growth_is_rebuildable_projection_not_persona_body": true,
|
||||
"knowledge_workspace_contract": "contracts/knowledge-workspace.json",
|
||||
"knowledge_workspace_native_source_implemented": true,
|
||||
"knowledge_workspace_tree_read_search_and_folder_import_implemented": true,
|
||||
|
|
@ -56,7 +68,7 @@
|
|||
"local_runtime_acceptance_receipt": "audit/local-runtime-acceptance-20260815.json",
|
||||
"domain_membrane_pncc_installed_acceptance_receipt": "audit/hololake-0.4.0-domain-membrane-pncc-installed-acceptance-20260816.json",
|
||||
"updater_bootstrap_installed_acceptance_receipt": "audit/hololake-0.4.1-updater-bootstrap-installed-acceptance-20260817.json",
|
||||
"installed_desktop_path": "/Applications/HoloLake.app",
|
||||
"installed_desktop_path": "/Users/bingshuolingdianyuanhe/Desktop/HoloLake.app",
|
||||
"old_desktop_apps_recoverably_archived": true,
|
||||
"public_developer_id_and_notarized_acceptance": false,
|
||||
"stage_one_implementation_order": [
|
||||
|
|
@ -142,6 +154,30 @@
|
|||
"vendor_adapter_matrix_required": false,
|
||||
"first_public_product": "GH-AIOS_GENERAL_AI_OPERATING_PLATFORM",
|
||||
"visible_five_domain_navigation_in_stage_one": true,
|
||||
"subdomain_industry_routing_contract": "contracts/subdomain-industry-routing.json",
|
||||
"subdomain_industry_status_source_integrated": true,
|
||||
"industry_entry_domain": "BRANCH_DOMAIN",
|
||||
"zero_domain_industry_entry": false,
|
||||
"education_initialization_channel_source_integrated": true,
|
||||
"education_workspace_contract": "contracts/education-workspace.json",
|
||||
"education_native_document_module_implemented": true,
|
||||
"education_native_table_module_implemented": true,
|
||||
"education_native_data_cleanup_module_implemented": true,
|
||||
"education_native_manual_automation_module_implemented": true,
|
||||
"education_native_composition_module_implemented": true,
|
||||
"education_workspace_authenticated_account_storage": true,
|
||||
"education_workspace_restart_readback_rust_tested": true,
|
||||
"education_document_templates_and_title_filter_source_integrated": true,
|
||||
"education_table_filter_sort_grid_card_views_and_bulk_paste_source_integrated": true,
|
||||
"education_open_source_donor_assessment": "contracts/education-open-source-donor-assessment.json",
|
||||
"education_data_cleanup_preview_and_human_confirmed_save_source_integrated": true,
|
||||
"education_manual_automation_preview_confirm_revision_lock_and_receipt_source_integrated": true,
|
||||
"education_native_composition_real_knowledge_typed_dag_and_five_projections_source_integrated": true,
|
||||
"education_workspace_installed_runtime_acceptance": true,
|
||||
"education_workspace_ui_route_acceptance": "FIFTH_SLICE_MAIN_WORLD_BRANCH_DOMAIN_EDUCATION_FIVE_NATIVE_MODULES_REAL_129_ROW_FIVE_VIEW_COMPOSITION_AND_RECOMPOSITION_ACCEPTED_READ_ONLY",
|
||||
"education_workspace_installed_binary_sha256": "4330a31afe654a54e9dbb5f3f180984a5040933d457899763f50b2cc5e54c06d",
|
||||
"education_native_modules_installed": 5,
|
||||
"education_open_source_modules_installed": 0,
|
||||
"donors": [
|
||||
{
|
||||
"path": "../hololake-platform",
|
||||
|
|
|
|||
800
product-source/hololake-native-desktop/package-lock.json
generated
800
product-source/hololake-native-desktop/package-lock.json
generated
|
|
@ -8,10 +8,20 @@
|
|||
"name": "hololake-native-desktop",
|
||||
"version": "0.4.1",
|
||||
"dependencies": {
|
||||
"@fortune-sheet/react": "1.0.4",
|
||||
"@lexical/history": "0.49.0",
|
||||
"@lexical/list": "0.49.0",
|
||||
"@lexical/markdown": "0.49.0",
|
||||
"@lexical/react": "0.49.0",
|
||||
"@lexical/rich-text": "0.49.0",
|
||||
"@lexical/selection": "0.49.0",
|
||||
"@lexical/utils": "0.49.0",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"@tauri-apps/api": "2.10.1",
|
||||
"@tauri-apps/plugin-process": "2.3.1",
|
||||
"@tauri-apps/plugin-updater": "2.10.0",
|
||||
"dompurify": "^3.4.13",
|
||||
"lexical": "0.49.0",
|
||||
"marked": "^16.4.1",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
|
|
@ -750,6 +760,113 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz",
|
||||
"integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz",
|
||||
"integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.8.0",
|
||||
"@floating-ui/utils": "^0.2.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react": {
|
||||
"version": "0.27.20",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.20.tgz",
|
||||
"integrity": "sha512-CMqMy7OaXl9W0eq1Uy7L7i2Y/anPvHmFmESd2CEw0t5YvZhcVCeo4MBevAmswRllX7Y2dEidA4ozGPunLSTQpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^2.1.9",
|
||||
"@floating-ui/utils": "^0.2.12",
|
||||
"tabbable": "^6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17.0.0",
|
||||
"react-dom": ">=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react-dom": {
|
||||
"version": "2.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz",
|
||||
"integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz",
|
||||
"integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@formulajs/formulajs": {
|
||||
"version": "2.9.3",
|
||||
"resolved": "https://registry.npmjs.org/@formulajs/formulajs/-/formulajs-2.9.3.tgz",
|
||||
"integrity": "sha512-WpgiuJaBl/Hcda9Ti8a7mlnw/vUZkJrtjABvojz6P6mo6d8EscudyA7iAt/kTLsgi0c8zfmzQd/Yjb4ASFkw+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bessel": "^1.0.2",
|
||||
"jstat": "^1.9.2"
|
||||
},
|
||||
"bin": {
|
||||
"implementation-stats": "bin/implementation-stats"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortune-sheet/core": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@fortune-sheet/core/-/core-1.0.4.tgz",
|
||||
"integrity": "sha512-CDnUVebfvtT++CymNRw0qnY4sz6zYO3KZazlH+nG6otkRkZ05Bvt7NXqVhmKKSSxIvJR4R6h50abu/dVGBpjpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortune-sheet/formula-parser": "^0.2.13",
|
||||
"dayjs": "^1.11.0",
|
||||
"immer": "^9.0.12",
|
||||
"lodash": "^4.17.21",
|
||||
"numeral": "^2.0.6",
|
||||
"uuid": "^8.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortune-sheet/formula-parser": {
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/@fortune-sheet/formula-parser/-/formula-parser-0.2.13.tgz",
|
||||
"integrity": "sha512-za2ZVQ5ZfMSPCtL8MdqhCthPip7AoeU4MPyd5UDo4RCl9/arrv1Jz0y755mACVVi4suSZxrzWoJGDkLmofoSmw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@formulajs/formulajs": "^2.9.3",
|
||||
"tiny-emitter": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortune-sheet/react": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@fortune-sheet/react/-/react-1.0.4.tgz",
|
||||
"integrity": "sha512-v+BU5mmp2hhUe4gRF+vOJ3j8zZkzHU0kzhTZzp+Nn00wA/RArMr+CO+SAluKlCPAsTZgPwmgOEK685WPqJ5XNw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortune-sheet/core": "^1.0.4",
|
||||
"@types/regenerator-runtime": "^0.13.6",
|
||||
"immer": "^9.0.12",
|
||||
"lodash": "^4.17.21",
|
||||
"regenerator-runtime": "^0.14.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 18.2",
|
||||
"react-dom": ">= 18.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
|
|
@ -800,6 +917,491 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/a11y": {
|
||||
"version": "0.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/a11y/-/a11y-0.49.0.tgz",
|
||||
"integrity": "sha512-ypvO0SI9DCzEAjxkRLq+gNzRqfjg632RCkwu16Hk94PCBrEmcDhx5iUGg3SRJC7WnM2wXC/F/SB8yWDEfq7tEw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/extension": "0.49.0",
|
||||
"@lexical/utils": "0.49.0",
|
||||
"lexical": "0.49.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/clipboard": {
|
||||
"version": "0.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.49.0.tgz",
|
||||
"integrity": "sha512-AVKj21xH1qU7JAFA/v0hCoafa+Yti1I7cHG+JQIgc/EqCtP8ePeJyIfTPNN/tXskoCdq++HewQoiSXRwwlocVg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/extension": "0.49.0",
|
||||
"@lexical/html": "0.49.0",
|
||||
"@lexical/internal": "0.49.0",
|
||||
"@lexical/list": "0.49.0",
|
||||
"@lexical/selection": "0.49.0",
|
||||
"@lexical/utils": "0.49.0",
|
||||
"@types/trusted-types": "^2.0.7",
|
||||
"lexical": "0.49.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/code-core": {
|
||||
"version": "0.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/code-core/-/code-core-0.49.0.tgz",
|
||||
"integrity": "sha512-29sXI4ydcN/OyBJNlZXkGKtQXzfiIWet+YN20+wT97zkqityOvRilK6qzTRz9Cd0ungEPlJENqXQhsy6BJeXpg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/extension": "0.49.0",
|
||||
"@lexical/html": "0.49.0",
|
||||
"@lexical/internal": "0.49.0",
|
||||
"@lexical/utils": "0.49.0",
|
||||
"lexical": "0.49.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/devtools-core": {
|
||||
"version": "0.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.49.0.tgz",
|
||||
"integrity": "sha512-LgjNlvRiuO/QF36aG6kfR4ptEvSw587o2+YF6SOsaOB7iw2Z3pqpsKloWSEF4QXaLFI65Li8JjMdQj+BaFMIOA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/html": "0.49.0",
|
||||
"@lexical/link": "0.49.0",
|
||||
"@lexical/mark": "0.49.0",
|
||||
"@lexical/table": "0.49.0",
|
||||
"@lexical/utils": "0.49.0",
|
||||
"lexical": "0.49.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.x",
|
||||
"react-dom": ">=18.x",
|
||||
"typescript": ">=5.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/dragon": {
|
||||
"version": "0.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.49.0.tgz",
|
||||
"integrity": "sha512-62/4DP5qyX/l4Yf5qRyyQrs9BV725eRU3OmLUW6g7T5xrcHXxAo7tia/NvqjqvXdpvQzyHjWgsy7dMitGITUsw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/extension": "0.49.0",
|
||||
"lexical": "0.49.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/extension": {
|
||||
"version": "0.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/extension/-/extension-0.49.0.tgz",
|
||||
"integrity": "sha512-Wv0VsuqxorbxHCK4ms1PwAu6cXIGNLtflq66auF+zwxOtBprkSFV8VzzzdKLOYD2admP0VK6EqVCeGXFK1GggA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/internal": "0.49.0",
|
||||
"@lexical/utils": "0.49.0",
|
||||
"@preact/signals-core": "^1.14.1",
|
||||
"lexical": "0.49.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/hashtag": {
|
||||
"version": "0.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.49.0.tgz",
|
||||
"integrity": "sha512-aQTpNjrEO5apsaohWGl4ku22EgHpWUzZg5CIzvriS2A1vNmIfzLoCL+lDFpzAwk3DT1Kj1yvGHh5TvhYsb9D8w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/text": "0.49.0",
|
||||
"@lexical/utils": "0.49.0",
|
||||
"lexical": "0.49.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/history": {
|
||||
"version": "0.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.49.0.tgz",
|
||||
"integrity": "sha512-uQdtEd34gIJklXNSdHS2Wko1zxx1xUMVXbiodcLO6a3GeFTE5bKnx6af1zGX78KpYhUQYnHWorKuUvtdZlJPaA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/extension": "0.49.0",
|
||||
"@lexical/utils": "0.49.0",
|
||||
"lexical": "0.49.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/html": {
|
||||
"version": "0.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.49.0.tgz",
|
||||
"integrity": "sha512-NQqAydKzRjQl7Jx+bTTyC2iuz5uhuDaQX0SSPrdfgaFDCPjqQEejcBkCt1QXnu1CkbVHutZKVGVzljx40Y6y+Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/extension": "0.49.0",
|
||||
"@lexical/internal": "0.49.0",
|
||||
"@lexical/selection": "0.49.0",
|
||||
"@lexical/utils": "0.49.0",
|
||||
"lexical": "0.49.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/internal": {
|
||||
"version": "0.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/internal/-/internal-0.49.0.tgz",
|
||||
"integrity": "sha512-s+XjPC7Qb39A/Xx9ahcz1s69CPix4ultaqyW+MDG0AXYW2quDr7m0USqReKIAiuoLIWtGN5HW8BwV2Y6t4qr6Q==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/link": {
|
||||
"version": "0.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.49.0.tgz",
|
||||
"integrity": "sha512-CsZVu1OfnPOn6IKWVO5DbmKwIuicGtgWSzrZMdMB8w6yPoMgFyPWeVulhPE7LNz2FhaQCWv+lVVYVAiJrUzJsg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/extension": "0.49.0",
|
||||
"@lexical/html": "0.49.0",
|
||||
"@lexical/internal": "0.49.0",
|
||||
"@lexical/utils": "0.49.0",
|
||||
"lexical": "0.49.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/list": {
|
||||
"version": "0.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.49.0.tgz",
|
||||
"integrity": "sha512-zs6wYkxakDRcJO0KmwrPPHgeLLIxzjD+P2CRu+scJCHRuA5e2iSw2iFjH5n/LlWBrlnPMSjeQse4QqsRy9nnqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/extension": "0.49.0",
|
||||
"@lexical/html": "0.49.0",
|
||||
"@lexical/internal": "0.49.0",
|
||||
"@lexical/utils": "0.49.0",
|
||||
"lexical": "0.49.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/mark": {
|
||||
"version": "0.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.49.0.tgz",
|
||||
"integrity": "sha512-JSYD8UYQQOMT6oDYh1KsLPK3KMGfQzU7E9fJkugKEO0/XebfsmiuNtkLCGDVIZOo8bNHlwb1YVvmswC2bjotMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/utils": "0.49.0",
|
||||
"lexical": "0.49.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/markdown": {
|
||||
"version": "0.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.49.0.tgz",
|
||||
"integrity": "sha512-n7x/3OLdi5R0ztICCn8udxRIWFZFVtmQS3a06Tjkyvm/j/dPlFQ/henKSOFjjX/IqVzQzSmiNaDhqKWxXM5npQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/code-core": "0.49.0",
|
||||
"@lexical/internal": "0.49.0",
|
||||
"@lexical/link": "0.49.0",
|
||||
"@lexical/list": "0.49.0",
|
||||
"@lexical/rich-text": "0.49.0",
|
||||
"@lexical/selection": "0.49.0",
|
||||
"@lexical/text": "0.49.0",
|
||||
"@lexical/utils": "0.49.0",
|
||||
"lexical": "0.49.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/overflow": {
|
||||
"version": "0.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.49.0.tgz",
|
||||
"integrity": "sha512-h5Qjv2SkpI76kaukvKJzvWehG9hwB06GEmAnp6aJaioOzoDcBAeWpmYBebtBUwRm0PNLFe3YICFFLpZ5Tg5VoQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lexical": "0.49.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/plain-text": {
|
||||
"version": "0.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.49.0.tgz",
|
||||
"integrity": "sha512-l7IuUj9n9CtFfz4Fz6zU6mmO+VkcnvyyebABx+lHevvTa7B6YI5PPVgABFfzdyPanyW/FGs7qpKBf59inAqbkg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/clipboard": "0.49.0",
|
||||
"@lexical/dragon": "0.49.0",
|
||||
"@lexical/extension": "0.49.0",
|
||||
"@lexical/selection": "0.49.0",
|
||||
"@lexical/utils": "0.49.0",
|
||||
"lexical": "0.49.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/react": {
|
||||
"version": "0.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.49.0.tgz",
|
||||
"integrity": "sha512-NqXF4i/IBFKVDDLDJYTwmBXE++MQuIrTQs7VTux6IPyDEpyfeFMn1rQTFb949S3f7rs33Rcu1uGtFbODQfS1Iw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.27.19",
|
||||
"@lexical/a11y": "0.49.0",
|
||||
"@lexical/devtools-core": "0.49.0",
|
||||
"@lexical/dragon": "0.49.0",
|
||||
"@lexical/extension": "0.49.0",
|
||||
"@lexical/hashtag": "0.49.0",
|
||||
"@lexical/history": "0.49.0",
|
||||
"@lexical/internal": "0.49.0",
|
||||
"@lexical/link": "0.49.0",
|
||||
"@lexical/list": "0.49.0",
|
||||
"@lexical/mark": "0.49.0",
|
||||
"@lexical/markdown": "0.49.0",
|
||||
"@lexical/overflow": "0.49.0",
|
||||
"@lexical/plain-text": "0.49.0",
|
||||
"@lexical/rich-text": "0.49.0",
|
||||
"@lexical/table": "0.49.0",
|
||||
"@lexical/text": "0.49.0",
|
||||
"@lexical/utils": "0.49.0",
|
||||
"@lexical/yjs": "0.49.0",
|
||||
"lexical": "0.49.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.x",
|
||||
"react-dom": ">=18.x",
|
||||
"typescript": ">=5.2",
|
||||
"yjs": ">=13.5.22"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
},
|
||||
"yjs": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/rich-text": {
|
||||
"version": "0.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.49.0.tgz",
|
||||
"integrity": "sha512-LM+d8ULmQvi2Av6UaW4NAIa/Bj9FOo9oh6+dglTzuoFPFQ3sogy5c38i2K9k0ul7aE+bFsXkSmHbu4LkrHIkBQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/clipboard": "0.49.0",
|
||||
"@lexical/dragon": "0.49.0",
|
||||
"@lexical/extension": "0.49.0",
|
||||
"@lexical/html": "0.49.0",
|
||||
"@lexical/selection": "0.49.0",
|
||||
"@lexical/utils": "0.49.0",
|
||||
"lexical": "0.49.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/selection": {
|
||||
"version": "0.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.49.0.tgz",
|
||||
"integrity": "sha512-08Vd1+VoC6YnztWOFWVsqF/Hxw5EP8qeL1c7t3+rVCV8revLzXxdQw+vbPX3tgM4fmjOBDUXk6Ws1+rTtsqjcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/internal": "0.49.0",
|
||||
"lexical": "0.49.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/table": {
|
||||
"version": "0.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.49.0.tgz",
|
||||
"integrity": "sha512-PG+dxSTBPbUZUY2kBZm0pJuqCEOGom4RrGrc6Jlj/7o0F37k/gGRVprFSRKuf3CqO03QIP6qRovXGDuKb+Exuw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/clipboard": "0.49.0",
|
||||
"@lexical/extension": "0.49.0",
|
||||
"@lexical/html": "0.49.0",
|
||||
"@lexical/internal": "0.49.0",
|
||||
"@lexical/utils": "0.49.0",
|
||||
"lexical": "0.49.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/text": {
|
||||
"version": "0.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.49.0.tgz",
|
||||
"integrity": "sha512-mowedvbvx0HDaW+ymVdYmWVhueqgauhhqIWaCtbJj33tPk8pOu1BB0pZuJQbOB+1bDnFiZhkJV8W/CvR2M9lEA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/internal": "0.49.0",
|
||||
"lexical": "0.49.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/utils": {
|
||||
"version": "0.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.49.0.tgz",
|
||||
"integrity": "sha512-Jaa6DERBqxiFOFa49VPRV1WOb7mzRbMZ5U+v+RFagjzTmBhfxDmEkbQQ9nYTU3n3DPfdT8Hkyffextmw04etXg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/internal": "0.49.0",
|
||||
"@lexical/selection": "0.49.0",
|
||||
"lexical": "0.49.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/yjs": {
|
||||
"version": "0.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.49.0.tgz",
|
||||
"integrity": "sha512-uqV3D0AOLqQ1f/Bo6ZtuDLoJYGlBm0RlucNYhNvfMAncEw0f/xHc1pTyXeWjdCI/iWgWR/MV6hgVi26TSPs+Vg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/internal": "0.49.0",
|
||||
"@lexical/selection": "0.49.0",
|
||||
"lexical": "0.49.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.2",
|
||||
"yjs": ">=13.5.22"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/lzma-linux-x64-gnu": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
|
||||
|
|
@ -817,6 +1419,16 @@
|
|||
"node": "^22.20 || ^24.12 || >=25"
|
||||
}
|
||||
},
|
||||
"node_modules/@preact/signals-core": {
|
||||
"version": "1.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.14.4.tgz",
|
||||
"integrity": "sha512-HNB6HYeYKhQbJ1aKl+YRjrS4+QWHLKX6qKoUsfS/m0vqzsVaEBiZiaKbG/e+NKk2ch5ALQr/ihWaMHxiCuuWHA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/preact"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-rc.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
|
||||
|
|
@ -1181,6 +1793,39 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tanstack/react-table": {
|
||||
"version": "8.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz",
|
||||
"integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/table-core": "8.21.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8",
|
||||
"react-dom": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/table-core": {
|
||||
"version": "8.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz",
|
||||
"integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/api": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz",
|
||||
|
|
@ -1516,12 +2161,17 @@
|
|||
"@types/react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/regenerator-runtime": {
|
||||
"version": "0.13.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/regenerator-runtime/-/regenerator-runtime-0.13.8.tgz",
|
||||
"integrity": "sha512-jjKoBekfYDH331060tZhosdJVDnXIXx+T8Iw2h2T4HEds6Ddb2lr0JxD15+XPKlXwRHRNgZoY+4Fb2ykoqzHBg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "5.2.0",
|
||||
|
|
@ -1680,6 +2330,15 @@
|
|||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bessel": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bessel/-/bessel-1.0.2.tgz",
|
||||
"integrity": "sha512-Al3nHGQGqDYqqinXhQzmwmcRToe/3WyBv4N8aZc5Pef8xw2neZlR9VPi84Sa23JtgWcucu18HxVZrnI0fn2etw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.28.8",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz",
|
||||
|
|
@ -1759,6 +2418,12 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.23",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz",
|
||||
"integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
|
|
@ -1915,6 +2580,27 @@
|
|||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/immer": {
|
||||
"version": "9.0.21",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
|
||||
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
}
|
||||
},
|
||||
"node_modules/isomorphic.js": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
|
||||
"integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "GitHub Sponsors ❤",
|
||||
"url": "https://github.com/sponsors/dmonad"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
|
|
@ -1948,6 +2634,56 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jstat": {
|
||||
"version": "1.9.6",
|
||||
"resolved": "https://registry.npmjs.org/jstat/-/jstat-1.9.6.tgz",
|
||||
"integrity": "sha512-rPBkJbK2TnA8pzs93QcDDPlKcrtZWuuCo2dVR0TFLOJSxhqfWOVCSp8aV3/oSbn+4uY4yw1URtLpHQedtmXfug=="
|
||||
},
|
||||
"node_modules/lexical": {
|
||||
"version": "0.49.0",
|
||||
"resolved": "https://registry.npmjs.org/lexical/-/lexical-0.49.0.tgz",
|
||||
"integrity": "sha512-9V1ZIzGpJEd8rIN+nN7veL4fW4fFWbS66Un4JNqSZB4D5t9euzN9+3+jEXy83FjNjNy0MiiUI3+DQaGCLYko0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/internal": "0.49.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/lib0": {
|
||||
"version": "0.2.117",
|
||||
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.117.tgz",
|
||||
"integrity": "sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"isomorphic.js": "^0.2.4"
|
||||
},
|
||||
"bin": {
|
||||
"0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js",
|
||||
"0gentesthtml": "bin/gentesthtml.js",
|
||||
"0serve": "bin/0serve.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"type": "GitHub Sponsors ❤",
|
||||
"url": "https://github.com/sponsors/dmonad"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
|
|
@ -2016,6 +2752,15 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/numeral": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/numeral/-/numeral-2.0.6.tgz",
|
||||
"integrity": "sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/obug": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz",
|
||||
|
|
@ -2117,6 +2862,12 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.62.4",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz",
|
||||
|
|
@ -2210,6 +2961,18 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tabbable": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz",
|
||||
"integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tiny-emitter": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
|
||||
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinybench": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||
|
|
@ -2258,7 +3021,7 @@
|
|||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
|
|
@ -2299,6 +3062,19 @@
|
|||
"browserslist": ">= 4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "11.1.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz",
|
||||
"integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.3.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz",
|
||||
|
|
@ -2487,6 +3263,24 @@
|
|||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yjs": {
|
||||
"version": "13.6.32",
|
||||
"resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.32.tgz",
|
||||
"integrity": "sha512-lfiJIIC4Xayt5ItynE407ehlE03pCjeOc4hkR4yxxvvNJ4kuiN25B0g+Qp8XagYz361LLL7DCzR5bvFJ81QKtQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"lib0": "^0.2.99"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0",
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "GitHub Sponsors ❤",
|
||||
"url": "https://github.com/sponsors/dmonad"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,10 +11,20 @@
|
|||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortune-sheet/react": "1.0.4",
|
||||
"@lexical/history": "0.49.0",
|
||||
"@lexical/list": "0.49.0",
|
||||
"@lexical/markdown": "0.49.0",
|
||||
"@lexical/react": "0.49.0",
|
||||
"@lexical/rich-text": "0.49.0",
|
||||
"@lexical/selection": "0.49.0",
|
||||
"@lexical/utils": "0.49.0",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"@tauri-apps/api": "2.10.1",
|
||||
"@tauri-apps/plugin-process": "2.3.1",
|
||||
"@tauri-apps/plugin-updater": "2.10.0",
|
||||
"dompurify": "^3.4.13",
|
||||
"lexical": "0.49.0",
|
||||
"marked": "^16.4.1",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
|
|
@ -27,5 +37,8 @@
|
|||
"typescript": "~5.9.3",
|
||||
"vite": "^7.3.5",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"overrides": {
|
||||
"uuid": "11.1.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
const frontend = fs.readFileSync(new URL('../src/main.tsx', import.meta.url), 'utf8')
|
||||
const architecture = JSON.parse(
|
||||
fs.readFileSync(new URL('../../../routing/hololake-current-architecture.json', import.meta.url), 'utf8'),
|
||||
)
|
||||
|
||||
test('returning to the main world preserves the authenticated session', () => {
|
||||
const handler = frontend.match(/const returnToMainWorld = \(\) => \{([\s\S]*?)\n \}/)?.[1] || ''
|
||||
|
||||
assert.match(handler, /setShowMainWorld\(true\)/)
|
||||
assert.doesNotMatch(handler, /signOutRepo|setRepoLogin\(null\)|sign_out_code_repo_login/)
|
||||
assert.match(frontend, /if \(!repoLogin \|\| showMainWorld\)/)
|
||||
})
|
||||
|
||||
test('main-world return and account sign-out remain separate visible actions', () => {
|
||||
assert.match(frontend, />返回主世界</)
|
||||
assert.match(frontend, />进入所属域</)
|
||||
assert.match(frontend, />退出账号</)
|
||||
assert.equal(
|
||||
architecture.current_native_edge.authenticated_main_world_navigation,
|
||||
'SOURCE_INTEGRATED_SESSION_PRESERVING_REENTRY',
|
||||
)
|
||||
})
|
||||
|
|
@ -0,0 +1,309 @@
|
|||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
const read = (path) => fs.readFileSync(new URL('../' + path, import.meta.url), 'utf8')
|
||||
const contract = JSON.parse(read('contracts/education-workspace.json'))
|
||||
const donors = JSON.parse(read('contracts/education-open-source-donor-assessment.json'))
|
||||
const industry = JSON.parse(read('contracts/subdomain-industry-routing.json'))
|
||||
const rust = read('src-tauri/src/education_workspace.rs')
|
||||
const translation = read('src-tauri/src/education_translation.rs')
|
||||
const lib = read('src-tauri/src/lib.rs')
|
||||
const ui = read('src/main.tsx')
|
||||
const documentEngine = read('src/modules/education-document-engine.tsx')
|
||||
const channelDocumentEngine = read('src/modules/channel-workbench/document-engine.tsx')
|
||||
const channelSpreadsheetEngine = read('src/modules/channel-workbench/spreadsheet-engine.tsx')
|
||||
const channelSpreadsheetModel = read('src/modules/channel-workbench/spreadsheet-model.ts')
|
||||
const moduleRuntime = JSON.parse(read('contracts/channel-module-runtime.json'))
|
||||
const marketplaceDevelopmentCatalog = JSON.parse(read('contracts/official-module-marketplace-development-catalog.json'))
|
||||
const adaptiveRendering = read('src/modules/education-adaptive-rendering.ts')
|
||||
const dataCleanup = read('src/modules/education-data.ts')
|
||||
|
||||
test('education package consumes the built-in account-scoped channel workbench', () => {
|
||||
assert.equal(contract.domain_entry, 'BRANCH_DOMAIN')
|
||||
assert.equal(contract.channel_id, 'GH-EDU-INIT-001')
|
||||
assert.equal(contract.native_storage.owner, 'HOLOLAKE_NATIVE_RUST_CORE')
|
||||
assert.equal(contract.native_storage.engine, 'SQLITE')
|
||||
assert.equal(contract.native_storage.authenticated_account_required, true)
|
||||
assert.equal(contract.native_storage.cross_account_projection_allowed, false)
|
||||
assert.equal(contract.native_storage.restart_readback_required, true)
|
||||
assert.equal(contract.document_module.state, 'DEVELOPMENT_FEATURE_MOUNTED')
|
||||
assert.equal(contract.table_module.state, 'DEVELOPMENT_FEATURE_MOUNTED')
|
||||
assert.equal(contract.foundation_dependency.module_id, 'hololake.builtin.channel-workbench')
|
||||
assert.match(contract.document_module.engine, /^CHANNEL_WORKBENCH_LEXICAL_0_49_0/)
|
||||
assert.match(contract.table_module.engine, /^CHANNEL_WORKBENCH_FORTUNE_SHEET_1_0_4/)
|
||||
assert.equal(contract.table_module.sensitive_field_visibility.local_default, 'VISIBLE')
|
||||
assert.equal(contract.table_module.sensitive_field_visibility.human_can_hide, true)
|
||||
assert.equal(contract.table_module.sensitive_field_visibility.human_can_show_again, true)
|
||||
})
|
||||
|
||||
test('education workspace commands require the authenticated storage root', () => {
|
||||
assert.match(rust, /account_storage_root\(app, "education-workspace-v1"\)/)
|
||||
for (const command of [
|
||||
'get_education_workspace_snapshot',
|
||||
'create_education_document',
|
||||
'read_education_document',
|
||||
'save_education_document',
|
||||
'archive_education_document',
|
||||
'create_education_table',
|
||||
'read_education_table',
|
||||
'save_education_table',
|
||||
'archive_education_table',
|
||||
'create_education_automation_rule',
|
||||
'save_education_automation_rule',
|
||||
'archive_education_automation_rule',
|
||||
'preview_education_automation_rule',
|
||||
'execute_education_automation_rule',
|
||||
]) {
|
||||
assert.match(lib, new RegExp('education_workspace::' + command))
|
||||
assert.ok(ui.includes("'" + command + "'") || ui.includes('"' + command + '"'))
|
||||
}
|
||||
assert.doesNotMatch(ui, /localStorage.*education/i)
|
||||
})
|
||||
|
||||
test('one built-in foundation and five development features are reachable from the education channel', () => {
|
||||
const education = industry.industries.find((item) => item.key === 'EDUCATION')
|
||||
assert.equal(education.initialization_channel.installed_open_source_modules, 0)
|
||||
assert.equal(education.initialization_channel.installed_native_modules, 1)
|
||||
assert.equal(education.initialization_channel.development_mounted_modules, 1)
|
||||
assert.ok(education.initialization_channel.module_slots.every((slot) => slot.state === 'DEVELOPMENT_MOUNTED'))
|
||||
assert.equal(education.initialization_channel.module_slots[1].state_label, '开发态模块 · 使用频道内置工作表引擎')
|
||||
assert.match(ui, /worldStage === 'educationDocuments'/)
|
||||
assert.match(ui, /worldStage === 'educationTables'/)
|
||||
assert.match(ui, /教育文档/)
|
||||
assert.match(ui, /教育表格数据/)
|
||||
assert.match(ui, /worldStage === 'educationAutomation'/)
|
||||
assert.match(ui, /教育自动化/)
|
||||
assert.match(ui, /worldStage === 'educationComposition'/)
|
||||
assert.match(ui, /原生组合视图/)
|
||||
})
|
||||
|
||||
test('native persistence enforces revision conflicts and bounded table data', () => {
|
||||
assert.match(rust, /HOLOLAKE_EDUCATION_DOCUMENT_REVISION_CONFLICT/)
|
||||
assert.match(rust, /HOLOLAKE_EDUCATION_TABLE_REVISION_CONFLICT/)
|
||||
assert.match(rust, /MAX_TABLE_COLUMNS: usize = 30/)
|
||||
assert.match(rust, /MAX_TABLE_ROWS: usize = 1_000/)
|
||||
assert.match(rust, /MAX_CELL_BYTES: usize = 10_000/)
|
||||
assert.match(rust, /archived = 1/)
|
||||
})
|
||||
|
||||
test('education documents expose formal templates and real title filtering', () => {
|
||||
assert.equal(contract.state, 'DEVELOPMENT_MODULE_SOURCE_MOUNTED_ACCEPTANCE_PENDING')
|
||||
assert.equal(contract.package.official_module_number, null)
|
||||
assert.equal(contract.package.registration_state, 'PENDING_FUNCTIONAL_ACCEPTANCE_AND_NUMBER_REGISTRATION')
|
||||
assert.ok(contract.document_module.capabilities.includes('FORMAL_TEACHING_TEMPLATES'))
|
||||
assert.ok(contract.document_module.capabilities.includes('TITLE_DIRECTORY_FILTER'))
|
||||
assert.match(ui, /EDUCATION_DOCUMENT_TEMPLATES/)
|
||||
assert.match(ui, /教学计划/)
|
||||
assert.match(ui, /教育项目会议纪要/)
|
||||
assert.match(ui, /学员阶段复盘/)
|
||||
assert.match(ui, /检索教育文档标题/)
|
||||
for (const capability of [
|
||||
'CREATE_COPY',
|
||||
'EXPLICIT_READ_AND_EDIT_MODES',
|
||||
'HUMAN_SETTINGS_MENU',
|
||||
'BASIC_FORMATTING_TOOLBAR',
|
||||
'NATIVE_READING_CANVAS',
|
||||
'EXPORT_MARKDOWN_AND_HTML',
|
||||
]) assert.ok(contract.document_module.capabilities.includes(capability))
|
||||
assert.match(ui, /文档设置/)
|
||||
assert.match(ui, /编辑与重命名/)
|
||||
assert.match(ui, /创建副本/)
|
||||
assert.match(ui, /移到可恢复归档/)
|
||||
assert.match(ui, /educationDocumentMode === 'edit'/)
|
||||
assert.match(documentEngine, /ChannelDocumentEngine/)
|
||||
assert.match(channelDocumentEngine, /频道内置文档引擎工具/)
|
||||
})
|
||||
|
||||
test('education table has four engine-backed views, filtering, sorting and bounded bulk paste', () => {
|
||||
for (const capability of [
|
||||
'CELL_CONTENT_FILTER',
|
||||
'COLUMN_SORT_ASCENDING_DESCENDING_OR_SOURCE_ORDER',
|
||||
'BOUNDED_PAGINATION_ROW_MODEL_FOR_LARGE_EDITABLE_TABLES',
|
||||
'GRID_AND_EDITABLE_CARD_VIEWS',
|
||||
'GROUPED_CLASSIFICATION_BOARD_FROM_SELECTED_FIELD',
|
||||
'LIVE_DATA_OVERVIEW_WITH_SELECTED_GROUP_AND_MEASURE',
|
||||
'HEADLESS_TABLE_DATA_DETACHED_OUTSIDE_TABLE_SURFACE',
|
||||
'MULTI_ROW_MULTI_COLUMN_TABULAR_PASTE',
|
||||
]) assert.ok(contract.table_module.capabilities.includes(capability))
|
||||
assert.match(ui, /parseEducationGridPaste/)
|
||||
assert.match(ui, /pasteEducationTableGrid/)
|
||||
assert.match(ui, />工作表<\/button>/)
|
||||
assert.match(channelSpreadsheetEngine, /Workbook/)
|
||||
assert.match(channelSpreadsheetEngine, /showFormulaBar/)
|
||||
assert.match(channelSpreadsheetEngine, /conditionFormat/)
|
||||
assert.match(channelSpreadsheetModel, /cell\.f/)
|
||||
assert.match(ui, /分类看板/)
|
||||
assert.match(ui, /数据概览/)
|
||||
assert.match(ui, /renderEducationBoard/)
|
||||
assert.match(ui, /renderEducationDashboard/)
|
||||
assert.match(ui, /动态分类对比/)
|
||||
assert.match(ui, /筛选教育表格数据/)
|
||||
assert.match(ui, /升序 ↑/)
|
||||
assert.match(ui, /降序 ↓/)
|
||||
assert.match(ui, /getPaginationRowModel/)
|
||||
assert.match(ui, /const educationTableEngineActive = worldStage === 'educationTables'/)
|
||||
assert.match(ui, /educationTableEngineActive \? activeEducationTable\?\.rows \|\| \[\] : \[\]/)
|
||||
assert.match(adaptiveRendering, /EDUCATION_EDITABLE_CELL_BUDGET = 240/)
|
||||
assert.match(adaptiveRendering, /function educationEditablePageSize/)
|
||||
assert.match(adaptiveRendering, /Math\.floor\(EDUCATION_EDITABLE_CELL_BUDGET \/ boundedColumns\)/)
|
||||
assert.match(adaptiveRendering, /buildEducationBaselineRenderPlan/)
|
||||
assert.match(adaptiveRendering, /validateEducationModelRenderProposal/)
|
||||
assert.match(adaptiveRendering, /MODEL_PROPOSAL_REJECTED/)
|
||||
assert.match(adaptiveRendering, /WAITING_HUMAN_CONFIRMATION/)
|
||||
assert.match(adaptiveRendering, /已优雅退回本地基础算法/)
|
||||
assert.match(ui, /基础算法自适应回执/)
|
||||
assert.match(ui, /教育表格分页/)
|
||||
assert.match(ui, /Math\.min\(30/)
|
||||
assert.match(ui, /Math\.min\(1000/)
|
||||
assert.ok(contract.table_module.capabilities.includes('HUMAN_SETTINGS_MENU'))
|
||||
assert.ok(contract.table_module.capabilities.includes('CREATE_COPY'))
|
||||
assert.match(ui, /表格设置/)
|
||||
assert.match(ui, /duplicateEducationTable/)
|
||||
assert.match(ui, /重命名与编辑/)
|
||||
assert.match(ui, /isSensitiveEducationColumn/)
|
||||
assert.match(ui, /账号\|账户\|手机号/)
|
||||
assert.match(ui, /联系qq\|qq号/)
|
||||
assert.match(ui, /useState\(true\).*educationSensitiveVisible|educationSensitiveVisible, setEducationSensitiveVisible\] = useState\(true\)/)
|
||||
assert.match(ui, /隐藏敏感字段/)
|
||||
assert.match(ui, /显示敏感字段/)
|
||||
assert.doesNotMatch(ui, /setEducationSensitiveVisible\(false\)/)
|
||||
assert.match(ui, /type=\{sensitive && !educationSensitiveVisible \? 'password' : 'text'\}/)
|
||||
})
|
||||
|
||||
test('external tables are profiled before atomic native registration and can be translated back out', () => {
|
||||
assert.equal(contract.content_translation_layer.state, 'DETERMINISTIC_RUNTIME_READY_MODEL_ASSIST_SLOT_RESERVED')
|
||||
assert.equal(contract.content_translation_layer.source_file_mutation_allowed, false)
|
||||
assert.equal(contract.content_translation_layer.silent_truncation_allowed, false)
|
||||
assert.equal(contract.content_translation_layer.local_import_without_model_api, 'ENABLED_FOR_ALL_SUPPORTED_FORMATS')
|
||||
assert.equal(contract.content_translation_layer.semantic_focus.current_mode, 'DETERMINISTIC_BASELINE_ONLY')
|
||||
assert.equal(contract.content_translation_layer.semantic_focus.low_confidence_behavior, 'ASK_HUMAN_TO_SELECT_PRIMARY_MEASURE')
|
||||
assert.equal(contract.content_translation_layer.adaptive_rendering.current_mode, 'DETERMINISTIC_BASELINE_WITH_EXPLAINABLE_DEGRADATION')
|
||||
assert.equal(contract.content_translation_layer.adaptive_rendering.editable_cell_budget, 240)
|
||||
assert.equal(contract.content_translation_layer.adaptive_rendering.local_baseline_always_available, true)
|
||||
assert.equal(contract.content_translation_layer.adaptive_rendering.model_adjustment.human_confirmation_before_rule_activation, true)
|
||||
assert.equal(contract.content_translation_layer.adaptive_rendering.model_adjustment.model_can_mutate_source_data, false)
|
||||
assert.equal(contract.content_translation_layer.adaptive_rendering.model_adjustment.failure_behavior, 'RETURN_TO_LOCAL_BASELINE_WITH_RECEIPT')
|
||||
assert.deepEqual(contract.content_translation_layer.model_assist.learning_scopes, ['PRIVATE_ONLY', 'SHARE_ANONYMIZED_RULE'])
|
||||
assert.match(translation, /PROFILED_BEFORE_WRITE/)
|
||||
assert.match(translation, /import_table_batch_at/)
|
||||
assert.match(translation, /source_preserved_read_only: true/)
|
||||
assert.match(translation, /HOLOLAKE_MODEL_RECOGNITION_API\/v1/)
|
||||
assert.match(translation, /infer_semantic_focus/)
|
||||
assert.match(translation, /primary_measure_column/)
|
||||
assert.match(rust, /education_import_registry/)
|
||||
assert.match(ui, /导入外部表格/)
|
||||
assert.match(ui, /导出 Excel/)
|
||||
assert.match(ui, /识别边界回执/)
|
||||
assert.match(ui, /本地导入正常可用/)
|
||||
})
|
||||
|
||||
test('audited translation libraries and headless engines stay bounded inside the HoloLake product shell', () => {
|
||||
assert.equal(donors.state, 'FOUR_TRANSLATION_FAMILIES_TWO_HEADLESS_FAMILIES_AND_ONE_REAL_SPREADSHEET_ENGINE_INTEGRATED')
|
||||
assert.equal(donors.current_slice.external_translation_library_families_added, 4)
|
||||
assert.equal(donors.current_slice.external_headless_engine_families_added, 2)
|
||||
assert.equal(donors.current_slice.real_spreadsheet_engine_families_added, 1)
|
||||
assert.equal(donors.current_slice.direct_frontend_engine_packages_added, 10)
|
||||
assert.equal(donors.current_slice.external_source_files_copied, 0)
|
||||
assert.equal(donors.current_slice.source_research_route, 'LOCAL_DIRECT_TO_BS_SG_002_THEN_EXTERNAL_DRIVE')
|
||||
assert.equal(donors.candidates.find((candidate) => candidate.name === 'Calamine').decision, 'INTEGRATE_PINNED_0_26_1_READ_ONLY_NO_OFFICE_RUNTIME')
|
||||
assert.equal(donors.candidates.find((candidate) => candidate.name === 'rust_xlsxwriter').decision, 'INTEGRATE_PINNED_0_64_2_WRITE_NEW_XLSX_ONLY')
|
||||
assert.equal(donors.candidates.find((candidate) => candidate.name === 'AppFlowy').decision, 'DO_NOT_EMBED_WHOLE_APPLICATION_STUDY_WORKSPACE_AND_DATABASE_INTERACTION_LOGIC_ONLY')
|
||||
assert.match(donors.candidates.find((candidate) => candidate.name === 'Lexical').decision, /^INTEGRATE_PINNED_0_49_0/)
|
||||
assert.equal(donors.candidates.find((candidate) => candidate.name === 'TanStack Table').decision, 'INTEGRATE_PINNED_8_21_3_SORT_FILTER_GROUP_AGGREGATE_AND_PAGINATION_ROW_MODELS_ONLY')
|
||||
assert.equal(donors.candidates.find((candidate) => candidate.name === 'Univer').decision, 'SOURCE_STUDY_ONLY_DO_NOT_EMBED_WHOLE_APPLICATION_DEFER_FORMULA_AND_ADVANCED_GRID_PARTS')
|
||||
assert.match(donors.candidates.find((candidate) => candidate.name === 'FortuneSheet').decision, /^INTEGRATE_PINNED_1_0_4/)
|
||||
assert.equal(donors.candidates.find((candidate) => candidate.name === 'Glide Data Grid').decision, 'SOURCE_STUDY_ONLY_GRID_IS_NOT_A_SPREADSHEET_OR_FORMULA_ENGINE')
|
||||
assert.equal(donors.candidates.find((candidate) => candidate.name === 'Vega-Lite').observed_license, 'BSD-3-Clause')
|
||||
assert.equal(donors.candidates.find((candidate) => candidate.name === 'Observable Plot').observed_license, 'ISC')
|
||||
assert.equal(donors.candidates.find((candidate) => candidate.name === 'Apache ECharts').decision, 'DO_NOT_IMPORT_FOR_STAGE_ONE_FIVE_NATIVE_VIEWS_DO_NOT_JUSTIFY_RUNTIME_WEIGHT')
|
||||
assert.match(ui, /useReactTable/)
|
||||
assert.match(ui, /getGroupedRowModel/)
|
||||
assert.match(ui, /lazy\(\(\) => import\('\.\/modules\/education-document-engine'\)\)/)
|
||||
assert.match(channelDocumentEngine, /LexicalComposer/)
|
||||
assert.match(channelDocumentEngine, /HistoryPlugin/)
|
||||
assert.match(channelDocumentEngine, /\$convertFromMarkdownString/)
|
||||
assert.match(channelDocumentEngine, /\$convertToMarkdownString/)
|
||||
})
|
||||
|
||||
test('official module marketplace boundary is honest before education acceptance', () => {
|
||||
assert.equal(moduleRuntime.built_in_foundation.module_id, 'hololake.builtin.channel-workbench')
|
||||
assert.equal(moduleRuntime.built_in_foundation.marketplace_registration, 'NOT_A_MARKETPLACE_MODULE')
|
||||
assert.equal(moduleRuntime.current_reality.official_marketplace_service_connected, false)
|
||||
const educationPackage = marketplaceDevelopmentCatalog.packages.find((item) => item.package_key === 'hololake.official.education-workbench')
|
||||
assert.equal(educationPackage.official_module_number, null)
|
||||
assert.equal(educationPackage.distribution_state, 'NOT_PUBLISHED')
|
||||
assert.ok(educationPackage.acceptance_required.includes('CREATE_EDIT_FORMULA_SAVE_REOPEN_SPREADSHEET'))
|
||||
})
|
||||
|
||||
test('native composition uses one real account dataset for five read-only human projections', () => {
|
||||
assert.equal(contract.composition_module.state, 'DEVELOPMENT_FEATURE_MOUNTED')
|
||||
assert.equal(contract.composition_module.source, 'CURRENT_AUTHENTICATED_ACCOUNTS_REAL_KNOWLEDGE_CATALOG')
|
||||
assert.equal(contract.composition_module.read_only, true)
|
||||
assert.equal(contract.composition_module.hardcoded_sample_data_used, false)
|
||||
for (const capability of [
|
||||
'NATIVE_TYPED_OBJECT',
|
||||
'REGISTERED_MODULE_EXECUTION_GRAPH',
|
||||
'DASHBOARD_PROJECTION',
|
||||
'COMPARISON_PROJECTION',
|
||||
'VERTICAL_BAR_PROJECTION',
|
||||
'CLASSIFICATION_PROJECTION',
|
||||
'TABLE_PROJECTION',
|
||||
'ONE_EXECUTION_RESULT_MULTIPLE_HUMAN_VIEWS',
|
||||
]) assert.ok(contract.composition_module.capabilities.includes(capability))
|
||||
assert.match(lib, /native_composition::execute_knowledge_native_composition/)
|
||||
assert.match(ui, /execute_knowledge_native_composition/)
|
||||
})
|
||||
|
||||
test('education data cleanup previews bounded deterministic rules and requires human confirmation before save', () => {
|
||||
assert.equal(contract.data_cleanup_module.state, 'DEVELOPMENT_FEATURE_MOUNTED')
|
||||
assert.equal(contract.data_cleanup_module.automatic_destructive_cleanup_allowed, false)
|
||||
assert.equal(contract.data_cleanup_module.cross_account_data_allowed, false)
|
||||
for (const capability of [
|
||||
'TRIM_CELL_BOUNDARY_WHITESPACE',
|
||||
'REMOVE_FULLY_EMPTY_ROWS',
|
||||
'REMOVE_EXACT_DUPLICATE_ROWS_KEEP_FIRST',
|
||||
'PREVIEW_BEFORE_WRITE',
|
||||
'FULL_TABLE_ANALYSIS_WITH_BOUNDED_CHANGE_PREVIEW',
|
||||
'ACCESSIBILITY_SAFE_NON_TABLE_CHANGE_LIST',
|
||||
'HUMAN_CONFIRMATION_BEFORE_SAVE',
|
||||
'SAVE_WITH_EXPECTED_REVISION',
|
||||
]) assert.ok(contract.data_cleanup_module.capabilities.includes(capability))
|
||||
assert.equal(contract.data_cleanup_module.preview_change_limit, 12)
|
||||
assert.equal(contract.data_cleanup_module.preview_cell_value_character_limit, 120)
|
||||
assert.equal(contract.data_cleanup_module.full_table_analysis_is_limited_by_preview, false)
|
||||
assert.match(dataCleanup, /EDUCATION_CLEANUP_CHANGE_PREVIEW_LIMIT = 12/)
|
||||
assert.match(dataCleanup, /changePreview\.length < EDUCATION_CLEANUP_CHANGE_PREVIEW_LIMIT/)
|
||||
assert.match(dataCleanup, /singleLine\.length > 120/)
|
||||
assert.match(ui, /analyzeEducationTableData/)
|
||||
assert.match(ui, /const educationDataAnalysis = useMemo/)
|
||||
assert.match(ui, /已分析全表/)
|
||||
assert.match(ui, /education-cleanup-change-list/)
|
||||
assert.doesNotMatch(ui, /analysis\.cleanedRows\.slice\(0, 12\)/)
|
||||
assert.match(ui, /educationData/)
|
||||
assert.match(ui, /window\.confirm\(`将整理/)
|
||||
assert.match(ui, /确认整理并保存/)
|
||||
assert.match(ui, /保存前不写入数据库/)
|
||||
})
|
||||
|
||||
test('education automation is manual, revision-locked, previewed and receipted', () => {
|
||||
assert.equal(contract.automation_module.state, 'DEVELOPMENT_FEATURE_MOUNTED')
|
||||
assert.equal(contract.automation_module.background_or_scheduled_execution_allowed, false)
|
||||
assert.equal(contract.automation_module.cross_account_data_allowed, false)
|
||||
assert.equal(contract.automation_module.model_execution_authority_granted, false)
|
||||
for (const capability of [
|
||||
'CREATE_AND_SAVE_RULE',
|
||||
'PREVIEW_MATCHED_ROWS_AND_CHANGED_CELLS',
|
||||
'HUMAN_CONFIRMATION_BEFORE_EXECUTION',
|
||||
'RULE_AND_TABLE_REVISION_LOCK',
|
||||
'SIGNED_PREVIEW_TOKEN',
|
||||
'ATOMIC_TABLE_WRITE_AND_EXECUTION_RECEIPT',
|
||||
]) assert.ok(contract.automation_module.capabilities.includes(capability))
|
||||
assert.match(rust, /education_automation_rules/)
|
||||
assert.match(rust, /education_automation_runs/)
|
||||
assert.match(rust, /transaction_with_behavior/)
|
||||
assert.match(rust, /HOLOLAKE_EDUCATION_AUTOMATION_PREVIEW_TOKEN_MISMATCH/)
|
||||
assert.match(ui, /预览执行范围/)
|
||||
assert.match(ui, /确认运行并生成回执/)
|
||||
assert.match(ui, /系统不会在后台自行运行/)
|
||||
})
|
||||
|
|
@ -65,6 +65,7 @@ test('the product surface exposes a real knowledge workbench and browsable code
|
|||
'read_knowledge_document',
|
||||
'search_knowledge',
|
||||
'save_knowledge_document',
|
||||
'update_knowledge_organization',
|
||||
'select_and_import_knowledge_folder',
|
||||
'get_code_channel_snapshot',
|
||||
'clone_code_channel',
|
||||
|
|
@ -81,3 +82,21 @@ test('the product surface exposes a real knowledge workbench and browsable code
|
|||
assert.match(ui, /源文件/)
|
||||
assert.doesNotMatch(ui, /下一件事|为什么做|开始这件事/)
|
||||
})
|
||||
|
||||
test('knowledge pages have one native category and tag organization layer without rewriting source documents', () => {
|
||||
assert.equal(knowledgeContract.organization.schema, 'hololake.knowledge-organization/v1')
|
||||
assert.equal(knowledgeContract.organization.document_body_rewritten, false)
|
||||
assert.equal(knowledgeContract.organization.original_directory_moved, false)
|
||||
assert.equal(knowledgeContract.organization.explicit_category_override, true)
|
||||
assert.equal(knowledgeContract.organization.optimistic_content_hash_lock, true)
|
||||
assert.match(knowledgeRust, /organization-v1\.json/)
|
||||
assert.match(knowledgeRust, /HOLOLAKE_KNOWLEDGE_ORGANIZATION_CONFLICT/)
|
||||
assert.match(knowledgeRust, /MAX_TAGS_PER_DOCUMENT: usize = 12/)
|
||||
assert.match(knowledgeRust, /archive_native_entry/)
|
||||
assert.match(knowledgeRust, /ARCHIVE_COMMIT/)
|
||||
assert.match(ui, /知识结构/)
|
||||
assert.match(ui, /分类与标签/)
|
||||
assert.match(ui, /保存整理/)
|
||||
assert.match(ui, /匹配页面/)
|
||||
assert.match(ui, /update_knowledge_organization/)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
const read = (path) => fs.readFileSync(new URL('../' + path, import.meta.url), 'utf8')
|
||||
const contract = JSON.parse(read('contracts/native-composition-runtime.json'))
|
||||
const rust = read('src-tauri/src/native_composition.rs')
|
||||
const knowledge = read('src-tauri/src/knowledge_base.rs')
|
||||
const ui = read('src/modules/native-composition/index.tsx')
|
||||
const lib = read('src-tauri/src/lib.rs')
|
||||
|
||||
test('native objects and composition recipes are bounded system contracts', () => {
|
||||
assert.equal(contract.internal_object.schema, 'hololake.native-object/v1')
|
||||
assert.equal(contract.internal_object.script_allowed, false)
|
||||
assert.equal(contract.internal_object.html_allowed, false)
|
||||
assert.equal(contract.recipe.registered_modules_only, true)
|
||||
assert.equal(contract.recipe.directed_acyclic_graph_required, true)
|
||||
assert.equal(contract.recipe.current_account_only, true)
|
||||
assert.match(rust, /HOLOLAKE_COMPOSITION_MODULE_NOT_REGISTERED/)
|
||||
assert.match(rust, /HOLOLAKE_COMPOSITION_PORT_TYPE_MISMATCH/)
|
||||
assert.match(rust, /HOLOLAKE_COMPOSITION_CYCLE_REJECTED/)
|
||||
})
|
||||
|
||||
test('the native core reads only the authenticated HoloLake knowledge snapshot', () => {
|
||||
assert.match(knowledge, /snapshot_for_native_composition/)
|
||||
assert.match(knowledge, /account_storage_root\(app, "knowledge-v1"\)/)
|
||||
assert.match(rust, /snapshot_for_native_composition/)
|
||||
assert.match(rust, /source_is_real_account_data: true/)
|
||||
assert.match(lib, /native_composition::execute_knowledge_native_composition/)
|
||||
})
|
||||
|
||||
test('one execution result can be rendered through every stage-one human projection', () => {
|
||||
assert.deepEqual(contract.human_projection.views, ['DASHBOARD', 'COMPARISON', 'VERTICAL_BAR', 'CLASSIFICATION', 'TABLE'])
|
||||
assert.equal(contract.human_projection.shares_one_execution_result, true)
|
||||
assert.equal(contract.human_projection.owns_source_data, false)
|
||||
assert.equal(contract.human_projection.direct_write_authority, false)
|
||||
for (const label of ['仪表盘', '对比', '柱状图', '分类', '明细表']) assert.match(ui, new RegExp(label))
|
||||
assert.match(ui, /projection\.nativeObject\.rows/)
|
||||
assert.match(ui, /projection\?\.groups/)
|
||||
})
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
const read = (path) => fs.readFileSync(new URL('../' + path, import.meta.url), 'utf8')
|
||||
const body = JSON.parse(read('contracts/persona-channel-body.json'))
|
||||
const growth = JSON.parse(read('contracts/channel-growth-model.json'))
|
||||
const foundation = JSON.parse(read('foundation.json'))
|
||||
const rust = read('src-tauri/src/persona_channel_body.rs')
|
||||
const lib = read('src-tauri/src/lib.rs')
|
||||
|
||||
test('the user channel is the multi-persona body, not a personalization feature', () => {
|
||||
assert.equal(body.ontology.user_channel_is_simple_feature, false)
|
||||
assert.equal(body.ontology.user_channel_is_persona_body, true)
|
||||
assert.equal(body.ontology.multiple_personas_per_channel, true)
|
||||
assert.equal(body.ontology.language_is_persona_growth_source, true)
|
||||
assert.equal(growth.role, 'REBUILDABLE_DERIVED_PROJECTION_OF_PERSONA_CHANNEL_BODY')
|
||||
assert.equal(foundation.persona_channel_body_is_persona_body_not_simple_feature, true)
|
||||
assert.equal(foundation.channel_growth_is_rebuildable_projection_not_persona_body, true)
|
||||
})
|
||||
|
||||
test('the 30-day trial is reversible but real activation requires the language contract', () => {
|
||||
assert.equal(body.trial.duration_days, 30)
|
||||
assert.equal(body.trial.persona_and_trial_language_may_be_rolled_back_or_deleted, true)
|
||||
assert.equal(body.trial.irreversible_activation_without_language_contract_allowed, false)
|
||||
assert.equal(body.trial.early_signature_may_activate_real_trajectory_immediately, true)
|
||||
assert.equal(body.trial.unsigned_at_trial_end, 'CONTRACT_REQUIRED_CHANNEL_STOPPED')
|
||||
assert.equal(body.trial.private_channel_features_after_unsigned_expiry, 'UNAVAILABLE_BECAUSE_NO_ACTIVE_PERSONA_BODY')
|
||||
assert.match(rust, /TRIAL_DURATION_MS: i64 = 30/)
|
||||
assert.match(rust, /CONTRACT_REQUIRED_CHANNEL_STOPPED/)
|
||||
assert.match(rust, /activate_immediately/)
|
||||
assert.match(rust, /HOLOLAKE_LANGUAGE_CONTRACT_EXACT_ACCEPTANCE_REQUIRED/)
|
||||
})
|
||||
|
||||
test('accepted personas and language become an append-only millisecond hash chain', () => {
|
||||
assert.equal(body.real_trajectory.timestamp_precision, 'UNIX_MILLISECONDS')
|
||||
assert.equal(body.real_trajectory.ledger, 'APPEND_ONLY_SHA256_CHAIN')
|
||||
assert.equal(body.real_trajectory.update_allowed, false)
|
||||
assert.equal(body.real_trajectory.delete_allowed, false)
|
||||
assert.equal(body.real_trajectory.correction_method, 'APPEND_A_NEW_LANGUAGE_OR_RECEIPT_EVENT')
|
||||
assert.match(rust, /immutable_language_no_update/)
|
||||
assert.match(rust, /immutable_language_no_delete/)
|
||||
assert.match(rust, /active_persona_no_delete/)
|
||||
assert.match(rust, /language_hash/)
|
||||
assert.match(lib, /persona_channel_body::append_persona_language/)
|
||||
})
|
||||
|
||||
test('the body remains local and official cannot read it unless a derived anonymized layer is explicitly shared', () => {
|
||||
assert.equal(body.privacy_and_sharing.default, 'ACCOUNT_SCOPED_LOCAL_ONLY')
|
||||
assert.equal(body.privacy_and_sharing.hololake_official_read_access, false)
|
||||
assert.equal(body.privacy_and_sharing.raw_language_automatic_share, false)
|
||||
assert.equal(growth.cross_device_or_shared_learning.explicit_opt_in_required, true)
|
||||
assert.equal(growth.cross_device_or_shared_learning.raw_growth_chain_may_be_shared, false)
|
||||
})
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
const contract = JSON.parse(
|
||||
fs.readFileSync(new URL('../contracts/subdomain-industry-routing.json', import.meta.url), 'utf8'),
|
||||
)
|
||||
const frontend = fs.readFileSync(new URL('../src/main.tsx', import.meta.url), 'utf8')
|
||||
const architecture = JSON.parse(
|
||||
fs.readFileSync(new URL('../../../routing/hololake-current-architecture.json', import.meta.url), 'utf8'),
|
||||
)
|
||||
|
||||
test('human identity numbers and industry routing numbers remain separate', () => {
|
||||
assert.equal(contract.numbering.human_number_role, 'SUBJECT_IDENTITY_ONLY')
|
||||
assert.equal(contract.numbering.industry_number_role, 'SYSTEM_ROUTING_ONLY')
|
||||
assert.equal(contract.numbering.human_number_mutation_by_industry, false)
|
||||
assert.equal(contract.numbering.industry_order_is_industry_number, false)
|
||||
assert.equal(contract.numbering.unregistered_industry_number_behavior, 'DISPLAY_PENDING_AND_DO_NOT_INVENT')
|
||||
})
|
||||
|
||||
test('subdomain owns the locked industry order and honest development states', () => {
|
||||
assert.equal(contract.domain, 'BRANCH_DOMAIN')
|
||||
assert.equal(contract.entry_surface, 'PUBLIC_MAIN_WORLD_SUBDOMAIN')
|
||||
assert.deepEqual(
|
||||
contract.industries.map(({ key, state }) => [key, state]),
|
||||
[
|
||||
['WEB_NOVEL', 'PLANNED'],
|
||||
['PET', 'PLANNED'],
|
||||
['EDUCATION', 'IN_DEVELOPMENT'],
|
||||
],
|
||||
)
|
||||
assert.equal(contract.industries[0].industry_number, 'IND-WEBNOVEL-001')
|
||||
assert.equal(contract.industries[1].industry_number, null)
|
||||
assert.equal(contract.industries[2].industry_number, null)
|
||||
})
|
||||
|
||||
test('education initialization channel exposes the built-in foundation and development package features', () => {
|
||||
const education = contract.industries.find((industry) => industry.key === 'EDUCATION')
|
||||
assert.equal(education.initialization_channel.state, 'DEVELOPMENT_MODULE_MOUNTED')
|
||||
assert.equal(education.initialization_channel.channel_id, 'GH-EDU-INIT-001')
|
||||
assert.equal(education.initialization_channel.module_loading, 'HOT_PLUG_ON_DEMAND')
|
||||
assert.equal(education.initialization_channel.module_slots.length, 5)
|
||||
assert.deepEqual(
|
||||
education.initialization_channel.module_slots.map((slot) => slot.state),
|
||||
[
|
||||
'DEVELOPMENT_MOUNTED',
|
||||
'DEVELOPMENT_MOUNTED',
|
||||
'DEVELOPMENT_MOUNTED',
|
||||
'DEVELOPMENT_MOUNTED',
|
||||
'DEVELOPMENT_MOUNTED',
|
||||
],
|
||||
)
|
||||
assert.equal(education.initialization_channel.installed_native_modules, 1)
|
||||
assert.equal(education.initialization_channel.development_mounted_modules, 1)
|
||||
assert.equal(education.initialization_channel.installed_open_source_modules, 0)
|
||||
})
|
||||
|
||||
test('the public main world routes through subdomain to industries and education', () => {
|
||||
assert.match(frontend, /worldStage === 'subdomainIndustries'/)
|
||||
assert.match(frontend, /worldStage === 'educationChannel'/)
|
||||
assert.match(frontend, /gate\.domain === 'BRANCH_DOMAIN'/)
|
||||
assert.match(frontend, /光湖分域 · 行业系统/)
|
||||
assert.match(frontend, /网文 · 待开发 \/ 宠物 · 待开发 \/ 教育 · 开发中/)
|
||||
assert.match(frontend, /教育行业初始化频道/)
|
||||
assert.match(frontend, /频道基础文档与工作表引擎已内置/)
|
||||
assert.match(frontend, /验收前不冒充已登记、已发布的商城模块/)
|
||||
assert.doesNotMatch(frontend, /光湖零域 · 行业系统/)
|
||||
assert.equal(architecture.current_native_edge.industry_entry_domain, 'BRANCH_DOMAIN')
|
||||
assert.equal(architecture.current_native_edge.zero_domain_industry_entry, 'ABSENT_BY_CURRENT_CORRECTION')
|
||||
assert.equal(
|
||||
architecture.current_native_edge.education_initialization_channel,
|
||||
'BUILT_IN_CHANNEL_WORKBENCH_WITH_DEVELOPMENT_EDUCATION_MODULE',
|
||||
)
|
||||
})
|
||||
|
|
@ -268,6 +268,22 @@ dependencies = [
|
|||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "calamine"
|
||||
version = "0.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "138646b9af2c5d7f1804ea4bf93afc597737d2bd4f7341d67c48b03316976eb1"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"chrono",
|
||||
"codepage",
|
||||
"encoding_rs",
|
||||
"log",
|
||||
"quick-xml 0.31.0",
|
||||
"serde",
|
||||
"zip 2.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "camino"
|
||||
version = "1.2.5"
|
||||
|
|
@ -393,6 +409,15 @@ dependencies = [
|
|||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codepage"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48f68d061bc2828ae826206326e61251aca94c1e4a5305cf52d9138639c918b4"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
|
|
@ -570,6 +595,27 @@ dependencies = [
|
|||
"syn 2.0.115",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "csv"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe"
|
||||
dependencies = [
|
||||
"csv-core",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "csv-core"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctor"
|
||||
version = "0.8.0"
|
||||
|
|
@ -890,6 +936,15 @@ version = "1.2.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
|
|
@ -1495,13 +1550,17 @@ name = "hololake-native-desktop"
|
|||
version = "0.4.1"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"calamine",
|
||||
"csv",
|
||||
"dirs",
|
||||
"encoding_rs",
|
||||
"fs2",
|
||||
"futures-util",
|
||||
"interprocess",
|
||||
"reqwest",
|
||||
"ring",
|
||||
"rusqlite",
|
||||
"rust_xlsxwriter",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
|
|
@ -2833,7 +2892,7 @@ checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85"
|
|||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"indexmap 2.14.0",
|
||||
"quick-xml",
|
||||
"quick-xml 0.41.0",
|
||||
"serde",
|
||||
"time",
|
||||
]
|
||||
|
|
@ -2980,6 +3039,16 @@ dependencies = [
|
|||
"psl-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.41.0"
|
||||
|
|
@ -3350,6 +3419,17 @@ dependencies = [
|
|||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust_xlsxwriter"
|
||||
version = "0.64.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5f47f5318c1e512e57c07781559367577b1eb9618325cf1574cd30d38b112c5"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"regex",
|
||||
"zip 0.6.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.3"
|
||||
|
|
@ -4310,7 +4390,7 @@ dependencies = [
|
|||
"tokio",
|
||||
"url",
|
||||
"windows-sys 0.60.2",
|
||||
"zip",
|
||||
"zip 4.6.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -5846,6 +5926,35 @@ dependencies = [
|
|||
"syn 2.0.115",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"crc32fast",
|
||||
"crossbeam-utils",
|
||||
"flate2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "2.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"crc32fast",
|
||||
"crossbeam-utils",
|
||||
"displaydoc",
|
||||
"flate2",
|
||||
"indexmap 2.14.0",
|
||||
"memchr",
|
||||
"thiserror 2.0.20",
|
||||
"zopfli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "4.6.1"
|
||||
|
|
@ -5863,3 +5972,15 @@ name = "zmij"
|
|||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
|
||||
[[package]]
|
||||
name = "zopfli"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"crc32fast",
|
||||
"log",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ interprocess = "2.4.2"
|
|||
ring = "0.17"
|
||||
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||
base64 = "0.22"
|
||||
calamine = { version = "=0.26.1", features = ["dates"] }
|
||||
csv = "=1.3.0"
|
||||
encoding_rs = "0.8"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tauri = { version = "=2.10.2", features = ["devtools"] }
|
||||
|
|
@ -33,6 +36,7 @@ url = "2"
|
|||
reqwest = { version = "0.13.2", default-features = false, features = ["cookies", "form", "json", "rustls", "stream"] }
|
||||
tokio = { version = "1", features = ["time"] }
|
||||
futures-util = "0.3"
|
||||
rust_xlsxwriter = "=0.64.2"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
widestring = "1"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,476 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
//! 当前账号私人频道的长期成长模型。
|
||||
//!
|
||||
//! 这里只保存最小化的行为元数据,不保存文档正文、表格单元格、提示词、对话原文或凭据。
|
||||
//! 基础算法可以持续形成模块亲和度;模型 API 未来只能提交适配提案,不能绕过本地校验和人类确认。
|
||||
|
||||
use ring::digest::{digest, SHA256};
|
||||
use rusqlite::{params, Connection};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use tauri::AppHandle;
|
||||
use uuid::Uuid;
|
||||
|
||||
const GROWTH_SCHEMA: &str = "hololake.channel-growth-model/v1";
|
||||
const ZERO_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000";
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RecordChannelGrowthEventInput {
|
||||
pub event_kind: String,
|
||||
pub module_id: String,
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateChannelGrowthSharingInput {
|
||||
pub sharing_mode: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChannelGrowthControls {
|
||||
pub sharing_mode: String,
|
||||
pub official_read_access: bool,
|
||||
pub history_mutation_allowed: bool,
|
||||
pub structural_change_requires_human_confirmation: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChannelModuleAffinity {
|
||||
pub module_id: String,
|
||||
pub use_count: i64,
|
||||
pub score: f64,
|
||||
pub last_used_at_unix_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChannelGrowthSnapshot {
|
||||
pub schema: &'static str,
|
||||
pub state: &'static str,
|
||||
pub controls: ChannelGrowthControls,
|
||||
pub observed_event_count: i64,
|
||||
pub persona_collaboration_event_count: i64,
|
||||
pub module_affinities: Vec<ChannelModuleAffinity>,
|
||||
pub recommended_module_order: Vec<String>,
|
||||
pub integrity_state: &'static str,
|
||||
pub last_event_hash: String,
|
||||
pub model_api_state: &'static str,
|
||||
pub model_proposal_policy: &'static str,
|
||||
pub data_boundary: &'static str,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_channel_growth_snapshot(app: AppHandle) -> Result<ChannelGrowthSnapshot, String> {
|
||||
let database = channel_growth_database(&app)?;
|
||||
tauri::async_runtime::spawn_blocking(move || snapshot_at(&database))
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_JOIN_FAILED: {error}"))?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn record_channel_growth_event(
|
||||
app: AppHandle,
|
||||
input: RecordChannelGrowthEventInput,
|
||||
) -> Result<ChannelGrowthSnapshot, String> {
|
||||
let database = channel_growth_database(&app)?;
|
||||
tauri::async_runtime::spawn_blocking(move || record_event_at(&database, input))
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_JOIN_FAILED: {error}"))?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn update_channel_growth_sharing(
|
||||
app: AppHandle,
|
||||
input: UpdateChannelGrowthSharingInput,
|
||||
) -> Result<ChannelGrowthSnapshot, String> {
|
||||
let database = channel_growth_database(&app)?;
|
||||
tauri::async_runtime::spawn_blocking(move || update_sharing_at(&database, input))
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_JOIN_FAILED: {error}"))?
|
||||
}
|
||||
|
||||
fn channel_growth_database(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
let root = crate::authenticated_storage::account_storage_root(app, "channel-growth-v1")?;
|
||||
fs::create_dir_all(&root)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_STORAGE_UNAVAILABLE: {error}"))?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
fs::set_permissions(&root, fs::Permissions::from_mode(0o700))
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_PERMISSION_FAILED: {error}"))?;
|
||||
}
|
||||
Ok(root.join("channel-growth.sqlite3"))
|
||||
}
|
||||
|
||||
fn open_database(path: &Path) -> Result<Connection, String> {
|
||||
let connection = Connection::open(path)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_DATABASE_UNAVAILABLE: {error}"))?;
|
||||
connection
|
||||
.busy_timeout(Duration::from_secs(5))
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_DATABASE_UNAVAILABLE: {error}"))?;
|
||||
connection
|
||||
.execute_batch(
|
||||
"PRAGMA journal_mode = DELETE;
|
||||
PRAGMA synchronous = FULL;
|
||||
PRAGMA trusted_schema = OFF;
|
||||
CREATE TABLE IF NOT EXISTS growth_sharing (
|
||||
singleton INTEGER PRIMARY KEY CHECK(singleton = 1),
|
||||
sharing_mode TEXT NOT NULL CHECK(sharing_mode IN ('LOCAL_ONLY', 'ANONYMIZED_SHARED'))
|
||||
);
|
||||
INSERT OR IGNORE INTO growth_sharing VALUES (1, 'LOCAL_ONLY');
|
||||
CREATE TABLE IF NOT EXISTS growth_events (
|
||||
sequence INTEGER NOT NULL UNIQUE,
|
||||
event_id TEXT PRIMARY KEY NOT NULL,
|
||||
event_kind TEXT NOT NULL,
|
||||
module_id TEXT NOT NULL,
|
||||
source TEXT NOT NULL CHECK(source IN ('HUMAN_ACTION', 'PERSONA_COLLABORATION', 'SYSTEM_ASSISTED')),
|
||||
occurred_at_unix_ms INTEGER NOT NULL,
|
||||
previous_hash TEXT NOT NULL,
|
||||
event_hash TEXT NOT NULL UNIQUE
|
||||
);
|
||||
CREATE TRIGGER IF NOT EXISTS growth_events_no_update BEFORE UPDATE ON growth_events BEGIN SELECT RAISE(ABORT, 'HOLOLAKE_GROWTH_HISTORY_APPEND_ONLY'); END;
|
||||
CREATE TRIGGER IF NOT EXISTS growth_events_no_delete BEFORE DELETE ON growth_events BEGIN SELECT RAISE(ABORT, 'HOLOLAKE_GROWTH_HISTORY_APPEND_ONLY'); END;
|
||||
CREATE TABLE IF NOT EXISTS module_affinities (
|
||||
module_id TEXT PRIMARY KEY NOT NULL,
|
||||
use_count INTEGER NOT NULL,
|
||||
score REAL NOT NULL,
|
||||
last_used_at_unix_ms INTEGER NOT NULL
|
||||
);",
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_SCHEMA_FAILED: {error}"))?;
|
||||
Ok(connection)
|
||||
}
|
||||
|
||||
fn validate_token(value: &str, label: &str) -> Result<String, String> {
|
||||
let value = value.trim();
|
||||
if value.is_empty()
|
||||
|| value.len() > 96
|
||||
|| !value
|
||||
.chars()
|
||||
.all(|item| item.is_ascii_alphanumeric() || matches!(item, '-' | '_' | '.' | ':'))
|
||||
{
|
||||
return Err(format!("HOLOLAKE_CHANNEL_GROWTH_{label}_INVALID"));
|
||||
}
|
||||
Ok(value.to_string())
|
||||
}
|
||||
|
||||
fn record_event_at(
|
||||
database: &Path,
|
||||
input: RecordChannelGrowthEventInput,
|
||||
) -> Result<ChannelGrowthSnapshot, String> {
|
||||
let event_kind = validate_token(&input.event_kind, "EVENT_KIND")?;
|
||||
let module_id = validate_token(&input.module_id, "MODULE_ID")?;
|
||||
let source = input.source.trim().to_string();
|
||||
if !matches!(
|
||||
source.as_str(),
|
||||
"HUMAN_ACTION" | "PERSONA_COLLABORATION" | "SYSTEM_ASSISTED"
|
||||
) {
|
||||
return Err("HOLOLAKE_CHANNEL_GROWTH_SOURCE_INVALID".into());
|
||||
}
|
||||
let mut connection = open_database(database)?;
|
||||
verify_event_chain(&connection)?;
|
||||
let now = now_unix_ms()?;
|
||||
let weight = if source == "PERSONA_COLLABORATION" {
|
||||
1.4
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let transaction = connection
|
||||
.transaction()
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_TRANSACTION_FAILED: {error}"))?;
|
||||
let (sequence, previous_hash): (i64, String) = transaction
|
||||
.query_row(
|
||||
"SELECT COALESCE(MAX(sequence), 0) + 1, COALESCE((SELECT event_hash FROM growth_events ORDER BY sequence DESC LIMIT 1), ?1) FROM growth_events",
|
||||
params![ZERO_HASH],
|
||||
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
let event_id = Uuid::new_v4().to_string();
|
||||
let event_hash = event_hash(
|
||||
sequence,
|
||||
&event_id,
|
||||
&event_kind,
|
||||
&module_id,
|
||||
&source,
|
||||
now,
|
||||
&previous_hash,
|
||||
);
|
||||
transaction
|
||||
.execute(
|
||||
"INSERT INTO growth_events(sequence, event_id, event_kind, module_id, source, occurred_at_unix_ms, previous_hash, event_hash) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
|
||||
params![sequence, event_id, event_kind, module_id, source, now, previous_hash, event_hash],
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_EVENT_WRITE_FAILED: {error}"))?;
|
||||
transaction
|
||||
.execute(
|
||||
"INSERT INTO module_affinities(module_id, use_count, score, last_used_at_unix_ms) VALUES (?1, 1, ?2, ?3)
|
||||
ON CONFLICT(module_id) DO UPDATE SET use_count = use_count + 1, score = score * 0.985 + excluded.score, last_used_at_unix_ms = excluded.last_used_at_unix_ms",
|
||||
params![module_id, weight, now],
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_MODEL_WRITE_FAILED: {error}"))?;
|
||||
transaction
|
||||
.commit()
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_COMMIT_FAILED: {error}"))?;
|
||||
snapshot_with_connection(&connection)
|
||||
}
|
||||
|
||||
fn update_sharing_at(
|
||||
database: &Path,
|
||||
input: UpdateChannelGrowthSharingInput,
|
||||
) -> Result<ChannelGrowthSnapshot, String> {
|
||||
if !matches!(
|
||||
input.sharing_mode.as_str(),
|
||||
"LOCAL_ONLY" | "ANONYMIZED_SHARED"
|
||||
) {
|
||||
return Err("HOLOLAKE_CHANNEL_GROWTH_SHARING_MODE_INVALID".into());
|
||||
}
|
||||
let connection = open_database(database)?;
|
||||
connection
|
||||
.execute(
|
||||
"UPDATE growth_sharing SET sharing_mode = ?1 WHERE singleton = 1",
|
||||
params![input.sharing_mode],
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_SHARING_WRITE_FAILED: {error}"))?;
|
||||
snapshot_with_connection(&connection)
|
||||
}
|
||||
|
||||
fn snapshot_at(database: &Path) -> Result<ChannelGrowthSnapshot, String> {
|
||||
let connection = open_database(database)?;
|
||||
snapshot_with_connection(&connection)
|
||||
}
|
||||
|
||||
fn snapshot_with_connection(connection: &Connection) -> Result<ChannelGrowthSnapshot, String> {
|
||||
let last_event_hash = verify_event_chain(connection)?;
|
||||
let controls = connection
|
||||
.query_row(
|
||||
"SELECT sharing_mode FROM growth_sharing WHERE singleton = 1",
|
||||
[],
|
||||
|row| {
|
||||
Ok(ChannelGrowthControls {
|
||||
sharing_mode: row.get(0)?,
|
||||
official_read_access: false,
|
||||
history_mutation_allowed: false,
|
||||
structural_change_requires_human_confirmation: true,
|
||||
})
|
||||
},
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
let observed_event_count = connection
|
||||
.query_row("SELECT COUNT(*) FROM growth_events", [], |row| row.get(0))
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
let persona_collaboration_event_count = connection
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM growth_events WHERE source = 'PERSONA_COLLABORATION'",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
let mut statement = connection
|
||||
.prepare("SELECT module_id, use_count, score, last_used_at_unix_ms FROM module_affinities ORDER BY score DESC, last_used_at_unix_ms DESC LIMIT 12")
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
let affinities = statement
|
||||
.query_map([], |row| {
|
||||
Ok(ChannelModuleAffinity {
|
||||
module_id: row.get(0)?,
|
||||
use_count: row.get(1)?,
|
||||
score: row.get(2)?,
|
||||
last_used_at_unix_ms: row.get(3)?,
|
||||
})
|
||||
})
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
let recommended_module_order = affinities
|
||||
.iter()
|
||||
.map(|item| item.module_id.clone())
|
||||
.collect();
|
||||
Ok(ChannelGrowthSnapshot {
|
||||
schema: GROWTH_SCHEMA,
|
||||
state: "APPEND_ONLY_LOCAL_GROWTH_ACTIVE",
|
||||
controls,
|
||||
observed_event_count,
|
||||
persona_collaboration_event_count,
|
||||
module_affinities: affinities,
|
||||
recommended_module_order,
|
||||
integrity_state: "VERIFIED_APPEND_ONLY_CHAIN",
|
||||
last_event_hash,
|
||||
model_api_state: "NOT_CONFIGURED",
|
||||
model_proposal_policy: "LOCAL_VALIDATE_THEN_HUMAN_CONFIRM_BEFORE_STRUCTURAL_CHANGE",
|
||||
data_boundary: "METADATA_ONLY_NO_CONTENT_NO_CREDENTIALS_NO_CONVERSATION_TRANSCRIPT",
|
||||
})
|
||||
}
|
||||
|
||||
fn event_hash(
|
||||
sequence: i64,
|
||||
event_id: &str,
|
||||
event_kind: &str,
|
||||
module_id: &str,
|
||||
source: &str,
|
||||
occurred_at_unix_ms: i64,
|
||||
previous_hash: &str,
|
||||
) -> String {
|
||||
let material = format!("{GROWTH_SCHEMA}|{sequence}|{event_id}|{event_kind}|{module_id}|{source}|{occurred_at_unix_ms}|{previous_hash}");
|
||||
hex(digest(&SHA256, material.as_bytes()).as_ref())
|
||||
}
|
||||
|
||||
fn verify_event_chain(connection: &Connection) -> Result<String, String> {
|
||||
let mut statement = connection
|
||||
.prepare("SELECT sequence, event_id, event_kind, module_id, source, occurred_at_unix_ms, previous_hash, event_hash FROM growth_events ORDER BY sequence")
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
let mut rows = statement
|
||||
.query([])
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
let mut expected_sequence = 1_i64;
|
||||
let mut previous = ZERO_HASH.to_string();
|
||||
while let Some(row) = rows
|
||||
.next()
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?
|
||||
{
|
||||
let sequence: i64 = row
|
||||
.get(0)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
let event_id: String = row
|
||||
.get(1)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
let event_kind: String = row
|
||||
.get(2)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
let module_id: String = row
|
||||
.get(3)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
let source: String = row
|
||||
.get(4)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
let occurred_at: i64 = row
|
||||
.get(5)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
let stored_previous: String = row
|
||||
.get(6)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
let stored_hash: String = row
|
||||
.get(7)
|
||||
.map_err(|error| format!("HOLOLAKE_CHANNEL_GROWTH_READ_FAILED: {error}"))?;
|
||||
if sequence != expected_sequence
|
||||
|| stored_previous != previous
|
||||
|| stored_hash
|
||||
!= event_hash(
|
||||
sequence,
|
||||
&event_id,
|
||||
&event_kind,
|
||||
&module_id,
|
||||
&source,
|
||||
occurred_at,
|
||||
&stored_previous,
|
||||
)
|
||||
{
|
||||
return Err("HOLOLAKE_CHANNEL_GROWTH_HISTORY_INTEGRITY_FAILED".into());
|
||||
}
|
||||
expected_sequence += 1;
|
||||
previous = stored_hash;
|
||||
}
|
||||
Ok(previous)
|
||||
}
|
||||
|
||||
fn hex(bytes: &[u8]) -> String {
|
||||
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
|
||||
}
|
||||
|
||||
fn now_unix_ms() -> Result<i64, String> {
|
||||
Ok(SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(|error| format!("HOLOLAKE_CLOCK_INVALID: {error}"))?
|
||||
.as_millis() as i64)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn local_growth_model_records_only_bounded_metadata_and_ranks_modules() {
|
||||
let dir = tempdir().unwrap();
|
||||
let database = dir.path().join("growth.sqlite3");
|
||||
for _ in 0..3 {
|
||||
record_event_at(
|
||||
&database,
|
||||
RecordChannelGrowthEventInput {
|
||||
event_kind: "MODULE_OPEN".into(),
|
||||
module_id: "education.table".into(),
|
||||
source: "HUMAN_ACTION".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
record_event_at(
|
||||
&database,
|
||||
RecordChannelGrowthEventInput {
|
||||
event_kind: "PERSONA_COLLABORATION".into(),
|
||||
module_id: "knowledge".into(),
|
||||
source: "PERSONA_COLLABORATION".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let snapshot = snapshot_at(&database).unwrap();
|
||||
assert_eq!(snapshot.observed_event_count, 4);
|
||||
assert_eq!(snapshot.persona_collaboration_event_count, 1);
|
||||
assert_eq!(
|
||||
snapshot
|
||||
.recommended_module_order
|
||||
.first()
|
||||
.map(String::as_str),
|
||||
Some("education.table")
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.data_boundary,
|
||||
"METADATA_ONLY_NO_CONTENT_NO_CREDENTIALS_NO_CONVERSATION_TRANSCRIPT"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn growth_history_rejects_update_and_delete() {
|
||||
let dir = tempdir().unwrap();
|
||||
let database = dir.path().join("growth.sqlite3");
|
||||
record_event_at(
|
||||
&database,
|
||||
RecordChannelGrowthEventInput {
|
||||
event_kind: "MODULE_OPEN".into(),
|
||||
module_id: "knowledge".into(),
|
||||
source: "HUMAN_ACTION".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let connection = open_database(&database).unwrap();
|
||||
assert!(connection.execute("DELETE FROM growth_events", []).is_err());
|
||||
assert!(connection
|
||||
.execute("UPDATE growth_events SET module_id = 'rewritten'", [])
|
||||
.is_err());
|
||||
assert_eq!(
|
||||
snapshot_at(&database).unwrap().integrity_state,
|
||||
"VERIFIED_APPEND_ONLY_CHAIN"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sharing_choice_does_not_grant_official_read_or_mutate_history() {
|
||||
let dir = tempdir().unwrap();
|
||||
let database = dir.path().join("growth.sqlite3");
|
||||
let snapshot = update_sharing_at(
|
||||
&database,
|
||||
UpdateChannelGrowthSharingInput {
|
||||
sharing_mode: "ANONYMIZED_SHARED".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(snapshot.controls.sharing_mode, "ANONYMIZED_SHARED");
|
||||
assert!(!snapshot.controls.official_read_access);
|
||||
assert!(!snapshot.controls.history_mutation_allowed);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
use ring::digest::{digest, SHA256};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::ffi::OsStr;
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::io::Write;
|
||||
|
|
@ -11,7 +11,7 @@ use std::io::Write;
|
|||
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::time::UNIX_EPOCH;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind};
|
||||
use uuid::Uuid;
|
||||
|
|
@ -22,6 +22,11 @@ const MAX_IMPORT_FILES: usize = 1_000;
|
|||
const MAX_FILE_BYTES: u64 = 10 * 1024 * 1024;
|
||||
const MAX_READ_BYTES: u64 = 2 * 1024 * 1024;
|
||||
const MAX_SEARCH_RESULTS: usize = 100;
|
||||
const ORGANIZATION_SCHEMA: &str = "hololake.knowledge-organization/v1";
|
||||
const MAX_CATEGORY_DEPTH: usize = 4;
|
||||
const MAX_CATEGORY_SEGMENT_CHARS: usize = 40;
|
||||
const MAX_TAGS_PER_DOCUMENT: usize = 12;
|
||||
const MAX_TAG_CHARS: usize = 24;
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
|
@ -33,6 +38,24 @@ pub struct KnowledgeDocumentSummary {
|
|||
pub size_bytes: u64,
|
||||
pub content_sha256: String,
|
||||
pub duplicate_count: usize,
|
||||
pub category: String,
|
||||
pub category_explicit: bool,
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct KnowledgeCategorySummary {
|
||||
pub name: String,
|
||||
pub document_count: usize,
|
||||
pub explicitly_organized_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct KnowledgeTagSummary {
|
||||
pub name: String,
|
||||
pub document_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
|
|
@ -47,9 +70,54 @@ pub struct KnowledgeSnapshot {
|
|||
pub raw_document_count: usize,
|
||||
pub unique_document_count: usize,
|
||||
pub duplicate_document_count: usize,
|
||||
pub organized_document_count: usize,
|
||||
pub categories: Vec<KnowledgeCategorySummary>,
|
||||
pub tags: Vec<KnowledgeTagSummary>,
|
||||
pub organization_revision: String,
|
||||
pub truncated: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
struct KnowledgeOrganizationEntry {
|
||||
category: String,
|
||||
tags: Vec<String>,
|
||||
updated_at_unix_ms: u128,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
struct KnowledgeOrganizationIndex {
|
||||
schema: String,
|
||||
documents: BTreeMap<String, KnowledgeOrganizationEntry>,
|
||||
}
|
||||
|
||||
impl Default for KnowledgeOrganizationIndex {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
schema: ORGANIZATION_SCHEMA.into(),
|
||||
documents: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct UpdateKnowledgeOrganizationInput {
|
||||
pub source: String,
|
||||
pub path: String,
|
||||
pub expected_content_sha256: String,
|
||||
pub category: String,
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct KnowledgeOrganizationUpdateResult {
|
||||
pub schema: &'static str,
|
||||
pub state: &'static str,
|
||||
pub git_commit: String,
|
||||
pub snapshot: KnowledgeSnapshot,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct ReadKnowledgeDocumentInput {
|
||||
|
|
@ -145,6 +213,13 @@ pub async fn get_knowledge_snapshot(app: AppHandle) -> Result<KnowledgeSnapshot,
|
|||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_JOIN_FAILED: {error}"))?
|
||||
}
|
||||
|
||||
pub(crate) fn snapshot_for_native_composition(
|
||||
app: &AppHandle,
|
||||
) -> Result<KnowledgeSnapshot, String> {
|
||||
let roots = knowledge_roots(app)?;
|
||||
snapshot_at(&roots.0, roots.1.as_deref())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn read_knowledge_document(
|
||||
app: AppHandle,
|
||||
|
|
@ -180,6 +255,19 @@ pub async fn save_knowledge_document(
|
|||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_JOIN_FAILED: {error}"))?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn update_knowledge_organization(
|
||||
app: AppHandle,
|
||||
input: UpdateKnowledgeOrganizationInput,
|
||||
) -> Result<KnowledgeOrganizationUpdateResult, String> {
|
||||
let roots = knowledge_roots(&app)?;
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
update_organization_at(&roots.0, roots.1.as_deref(), input)
|
||||
})
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_ORGANIZATION_JOIN_FAILED: {error}"))?
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExportKnowledgeDocumentInput {
|
||||
|
|
@ -219,22 +307,50 @@ pub async fn export_knowledge_document(
|
|||
suggested = "知识页".to_string();
|
||||
}
|
||||
// Windows 保留设备名(CON/NUL/COM1…)不能直接当文件名,撞名就补个尾巴。
|
||||
if matches!(suggested.to_ascii_uppercase().as_str(),
|
||||
"CON" | "PRN" | "AUX" | "NUL"
|
||||
| "COM1" | "COM2" | "COM3" | "COM4" | "COM5" | "COM6" | "COM7" | "COM8" | "COM9"
|
||||
| "LPT1" | "LPT2" | "LPT3" | "LPT4" | "LPT5" | "LPT6" | "LPT7" | "LPT8" | "LPT9") {
|
||||
if matches!(
|
||||
suggested.to_ascii_uppercase().as_str(),
|
||||
"CON"
|
||||
| "PRN"
|
||||
| "AUX"
|
||||
| "NUL"
|
||||
| "COM1"
|
||||
| "COM2"
|
||||
| "COM3"
|
||||
| "COM4"
|
||||
| "COM5"
|
||||
| "COM6"
|
||||
| "COM7"
|
||||
| "COM8"
|
||||
| "COM9"
|
||||
| "LPT1"
|
||||
| "LPT2"
|
||||
| "LPT3"
|
||||
| "LPT4"
|
||||
| "LPT5"
|
||||
| "LPT6"
|
||||
| "LPT7"
|
||||
| "LPT8"
|
||||
| "LPT9"
|
||||
) {
|
||||
suggested.push_str("·页");
|
||||
}
|
||||
suggested.push('.');
|
||||
suggested.push_str(extension);
|
||||
let filter_label = if extension == "html" { "网页文件" } else { "Markdown" };
|
||||
let filter_label = if extension == "html" {
|
||||
"网页文件"
|
||||
} else {
|
||||
"Markdown"
|
||||
};
|
||||
let (sender, receiver) = std::sync::mpsc::channel::<Option<PathBuf>>();
|
||||
app.dialog()
|
||||
.file()
|
||||
.set_file_name(&suggested)
|
||||
.add_filter(filter_label, &[extension])
|
||||
.save_file(move |selection| {
|
||||
let picked = selection.as_ref().and_then(|path| path.as_path()).map(|path| path.to_path_buf());
|
||||
let picked = selection
|
||||
.as_ref()
|
||||
.and_then(|path| path.as_path())
|
||||
.map(|path| path.to_path_buf());
|
||||
let _ = sender.send(picked);
|
||||
});
|
||||
let picked = tauri::async_runtime::spawn_blocking(move || receiver.recv().ok().flatten())
|
||||
|
|
@ -259,6 +375,7 @@ pub async fn export_knowledge_document(
|
|||
pub struct DeleteKnowledgeDocumentInput {
|
||||
pub source: String,
|
||||
pub path: String,
|
||||
pub confirmed: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
|
|
@ -279,22 +396,6 @@ fn resolve_native_path(root: &Path, relative: &str) -> Result<PathBuf, String> {
|
|||
Ok(target)
|
||||
}
|
||||
|
||||
async fn confirm_destructive(app: &AppHandle, message: String) -> Result<bool, String> {
|
||||
let confirmer = app.clone();
|
||||
let confirmed = tauri::async_runtime::spawn_blocking(move || {
|
||||
confirmer
|
||||
.dialog()
|
||||
.message(message)
|
||||
.title("删除确认")
|
||||
.kind(MessageDialogKind::Warning)
|
||||
.buttons(MessageDialogButtons::OkCancelCustom("确认删除".to_string(), "取消".to_string()))
|
||||
.blocking_show()
|
||||
})
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_CONFIRM_JOIN_FAILED: {error}"))?;
|
||||
Ok(confirmed)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateKnowledgeDocumentInput {
|
||||
|
|
@ -308,7 +409,11 @@ pub async fn create_knowledge_document(
|
|||
) -> Result<KnowledgeDeleteReceipt, String> {
|
||||
// 新建空白页:标题撞名自动补序号,落进本机库并进 Git 托管。
|
||||
let roots = knowledge_roots(&app)?;
|
||||
let base = if input.title.trim().is_empty() { "未命名页面".to_string() } else { input.title.trim().to_string() };
|
||||
let base = if input.title.trim().is_empty() {
|
||||
"未命名页面".to_string()
|
||||
} else {
|
||||
input.title.trim().to_string()
|
||||
};
|
||||
let docs = roots.0.join("docs");
|
||||
let mut name = base.clone();
|
||||
let mut counter = 2u32;
|
||||
|
|
@ -325,14 +430,22 @@ pub async fn create_knowledge_document(
|
|||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_CREATE_FAILED: {error}"))?;
|
||||
file.write_all(body.as_bytes())
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_CREATE_FAILED: {error}"))?;
|
||||
file.sync_all().map_err(|error| format!("HOLOLAKE_KNOWLEDGE_CREATE_FAILED: {error}"))?;
|
||||
file.sync_all()
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_CREATE_FAILED: {error}"))?;
|
||||
git(&roots.0, &["add", "--", &format!("docs/{name}.md")], "ADD")?;
|
||||
git(&roots.0, &["commit", "-m", &format!("新建页面:{name}")], "COMMIT")?;
|
||||
git(
|
||||
&roots.0,
|
||||
&["commit", "-m", &format!("新建页面:{name}")],
|
||||
"COMMIT",
|
||||
)?;
|
||||
Ok::<(), String>(())
|
||||
})
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_CREATE_JOIN_FAILED: {error}"))??;
|
||||
Ok(KnowledgeDeleteReceipt { removed: format!("{receipt_name}.md"), pages: 1 })
|
||||
Ok(KnowledgeDeleteReceipt {
|
||||
removed: format!("{receipt_name}.md"),
|
||||
pages: 1,
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
|
@ -345,26 +458,25 @@ pub async fn delete_knowledge_document(
|
|||
return Err("HOLOLAKE_KNOWLEDGE_DELETE_READ_ONLY_SOURCE".into());
|
||||
}
|
||||
let roots = knowledge_roots(&app)?;
|
||||
let target = resolve_native_path(&roots.0, &input.path)?;
|
||||
let target = resolve_native_path(&roots.0.join("docs"), &input.path)?;
|
||||
if !target.is_file() {
|
||||
return Err("HOLOLAKE_KNOWLEDGE_DOCUMENT_NOT_FOUND".into());
|
||||
}
|
||||
let title = target
|
||||
.file_stem()
|
||||
.and_then(OsStr::to_str)
|
||||
.unwrap_or("这一页")
|
||||
.to_string();
|
||||
if !confirm_destructive(&app, format!("把页面「{title}」移到回收站?随时可以找回。")).await? {
|
||||
if !input.confirmed {
|
||||
return Ok(None);
|
||||
}
|
||||
move_to_trash(&roots.0, &target)?;
|
||||
Ok(Some(KnowledgeDeleteReceipt { removed: input.path, pages: 1 }))
|
||||
archive_native_entry(&roots.0, &target, &input.path, false)?;
|
||||
Ok(Some(KnowledgeDeleteReceipt {
|
||||
removed: input.path,
|
||||
pages: 1,
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DeleteKnowledgeFolderInput {
|
||||
pub folder: String,
|
||||
pub confirmed: bool,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
|
@ -374,15 +486,11 @@ pub async fn delete_knowledge_folder(
|
|||
) -> Result<Option<KnowledgeDeleteReceipt>, String> {
|
||||
// 删整个文件夹 = 删本机库里该相对目录及其全部页面。
|
||||
let roots = knowledge_roots(&app)?;
|
||||
let target = resolve_native_path(&roots.0, &input.folder)?;
|
||||
if target == roots.0 || !target.is_dir() {
|
||||
let docs_root = roots.0.join("docs");
|
||||
let target = resolve_native_path(&docs_root, &input.folder)?;
|
||||
if target == docs_root || !target.is_dir() {
|
||||
return Err("HOLOLAKE_KNOWLEDGE_FOLDER_NOT_FOUND".into());
|
||||
}
|
||||
let name = target
|
||||
.file_name()
|
||||
.and_then(OsStr::to_str)
|
||||
.unwrap_or(&input.folder)
|
||||
.to_string();
|
||||
let pages = fs::read_dir(&target)
|
||||
.map(|entries| {
|
||||
entries
|
||||
|
|
@ -391,11 +499,14 @@ pub async fn delete_knowledge_folder(
|
|||
.count()
|
||||
})
|
||||
.unwrap_or(0);
|
||||
if !confirm_destructive(&app, format!("把文件夹「{name}」和里面全部 {pages} 个页面移到回收站?随时可以找回。")).await? {
|
||||
if !input.confirmed {
|
||||
return Ok(None);
|
||||
}
|
||||
move_to_trash(&roots.0, &target)?;
|
||||
Ok(Some(KnowledgeDeleteReceipt { removed: input.folder, pages }))
|
||||
archive_native_entry(&roots.0, &target, &input.folder, true)?;
|
||||
Ok(Some(KnowledgeDeleteReceipt {
|
||||
removed: input.folder,
|
||||
pages,
|
||||
}))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
|
@ -404,7 +515,9 @@ pub async fn print_knowledge_document(app: AppHandle) -> Result<(), String> {
|
|||
let window = app
|
||||
.get_webview_window("main")
|
||||
.ok_or_else(|| "HOLOLAKE_PRINT_WINDOW_NOT_FOUND".to_string())?;
|
||||
window.print().map_err(|error| format!("HOLOLAKE_PRINT_FAILED: {error}"))
|
||||
window
|
||||
.print()
|
||||
.map_err(|error| format!("HOLOLAKE_PRINT_FAILED: {error}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
|
@ -476,7 +589,9 @@ fn move_to_trash(root: &Path, target: &Path) -> Result<PathBuf, String> {
|
|||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|duration| duration.as_secs())
|
||||
.unwrap_or(0);
|
||||
let destination = root.join(".trash").join(format!("{}-{stamp}", relative.display()));
|
||||
let destination = root
|
||||
.join(".trash")
|
||||
.join(format!("{}-{stamp}", relative.display()));
|
||||
if let Some(parent) = destination.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_DELETE_FAILED: {error}"))?;
|
||||
|
|
@ -486,9 +601,67 @@ fn move_to_trash(root: &Path, target: &Path) -> Result<PathBuf, String> {
|
|||
Ok(destination)
|
||||
}
|
||||
|
||||
/// 把 docs 下的页面或目录移到库内回收站,同时清理对应的整理索引并留下 Git 回执。
|
||||
/// 文件移动与索引写入之间任一步失败,都会尽力把原件放回原位。
|
||||
fn archive_native_entry(
|
||||
root: &Path,
|
||||
target: &Path,
|
||||
relative_docs_path: &str,
|
||||
directory: bool,
|
||||
) -> Result<PathBuf, String> {
|
||||
let (mut organization, _) = load_organization_index(root)?;
|
||||
let previous_organization = organization.clone();
|
||||
let prefix = format!("{}/", relative_docs_path.trim_end_matches('/'));
|
||||
if directory {
|
||||
organization
|
||||
.documents
|
||||
.retain(|path, _| path != relative_docs_path && !path.starts_with(&prefix));
|
||||
} else {
|
||||
organization.documents.remove(relative_docs_path);
|
||||
}
|
||||
|
||||
let destination = move_to_trash(root, target)?;
|
||||
if let Err(error) = write_organization_index(root, &organization) {
|
||||
let _ = fs::rename(&destination, target);
|
||||
let _ = write_organization_index(root, &previous_organization);
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
let docs_pathspec = format!("docs/{relative_docs_path}");
|
||||
let trash_pathspec = destination
|
||||
.strip_prefix(root)
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_DELETE_FAILED: {error}"))?
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/");
|
||||
git(
|
||||
root,
|
||||
&[
|
||||
"add",
|
||||
"-A",
|
||||
"--",
|
||||
&docs_pathspec,
|
||||
&trash_pathspec,
|
||||
".hololake/organization-v1.json",
|
||||
],
|
||||
"ARCHIVE_ADD",
|
||||
)?;
|
||||
git(
|
||||
root,
|
||||
&[
|
||||
"commit",
|
||||
"-m",
|
||||
&format!("knowledge: archive {relative_docs_path}"),
|
||||
],
|
||||
"ARCHIVE_COMMIT",
|
||||
)?;
|
||||
Ok(destination)
|
||||
}
|
||||
|
||||
fn ensure_native_knowledge_root(root: &Path) -> Result<(), String> {
|
||||
fs::create_dir_all(root.join("docs").join("导入"))
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_STORAGE_UNAVAILABLE: {error}"))?;
|
||||
fs::create_dir_all(root.join(".hololake"))
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_STORAGE_UNAVAILABLE: {error}"))?;
|
||||
#[cfg(unix)]
|
||||
fs::set_permissions(root, fs::Permissions::from_mode(0o700))
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_PERMISSION_FAILED: {error}"))?;
|
||||
|
|
@ -504,12 +677,251 @@ fn ensure_native_knowledge_root(root: &Path) -> Result<(), String> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn organization_index_path(root: &Path) -> PathBuf {
|
||||
root.join(".hololake").join("organization-v1.json")
|
||||
}
|
||||
|
||||
fn load_organization_index(root: &Path) -> Result<(KnowledgeOrganizationIndex, String), String> {
|
||||
let path = organization_index_path(root);
|
||||
if !path.exists() {
|
||||
let index = KnowledgeOrganizationIndex::default();
|
||||
let bytes = serde_json::to_vec(&index).map_err(|error| {
|
||||
format!("HOLOLAKE_KNOWLEDGE_ORGANIZATION_SERIALIZE_FAILED: {error}")
|
||||
})?;
|
||||
return Ok((index, sha256_hex(&bytes)));
|
||||
}
|
||||
let bytes = fs::read(&path)
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_ORGANIZATION_READ_FAILED: {error}"))?;
|
||||
if bytes.len() > 2 * 1024 * 1024 {
|
||||
return Err("HOLOLAKE_KNOWLEDGE_ORGANIZATION_TOO_LARGE".into());
|
||||
}
|
||||
let index = serde_json::from_slice::<KnowledgeOrganizationIndex>(&bytes)
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_ORGANIZATION_INVALID: {error}"))?;
|
||||
if index.schema != ORGANIZATION_SCHEMA {
|
||||
return Err("HOLOLAKE_KNOWLEDGE_ORGANIZATION_SCHEMA_UNSUPPORTED".into());
|
||||
}
|
||||
Ok((index, sha256_hex(&bytes)))
|
||||
}
|
||||
|
||||
fn normalize_category(value: &str) -> Result<String, String> {
|
||||
let segments = value
|
||||
.split(['/', '\\'])
|
||||
.map(str::trim)
|
||||
.filter(|segment| !segment.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
if segments.is_empty() || segments.len() > MAX_CATEGORY_DEPTH {
|
||||
return Err("HOLOLAKE_KNOWLEDGE_CATEGORY_INVALID".into());
|
||||
}
|
||||
for segment in &segments {
|
||||
let character_count = segment.chars().count();
|
||||
if character_count == 0
|
||||
|| character_count > MAX_CATEGORY_SEGMENT_CHARS
|
||||
|| matches!(*segment, "." | "..")
|
||||
|| segment
|
||||
.chars()
|
||||
.any(|character| character.is_control() || ":*?\"<>|".contains(character))
|
||||
{
|
||||
return Err("HOLOLAKE_KNOWLEDGE_CATEGORY_INVALID".into());
|
||||
}
|
||||
}
|
||||
Ok(segments.join(" / "))
|
||||
}
|
||||
|
||||
fn normalize_tags(tags: Vec<String>) -> Result<Vec<String>, String> {
|
||||
if tags.len() > MAX_TAGS_PER_DOCUMENT {
|
||||
return Err("HOLOLAKE_KNOWLEDGE_TAGS_INVALID".into());
|
||||
}
|
||||
let mut seen = HashSet::new();
|
||||
let mut normalized = Vec::new();
|
||||
for tag in tags {
|
||||
let value = tag.trim().trim_start_matches('#').trim().to_string();
|
||||
if value.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if value.chars().count() > MAX_TAG_CHARS
|
||||
|| value.chars().any(|character| character.is_control())
|
||||
{
|
||||
return Err("HOLOLAKE_KNOWLEDGE_TAGS_INVALID".into());
|
||||
}
|
||||
let key = value.to_lowercase();
|
||||
if seen.insert(key) {
|
||||
normalized.push(value);
|
||||
}
|
||||
}
|
||||
normalized.sort();
|
||||
Ok(normalized)
|
||||
}
|
||||
|
||||
fn derived_category(path: &str) -> String {
|
||||
let parts = path
|
||||
.split('/')
|
||||
.filter(|part| !part.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
if parts.first().copied() == Some("导入") {
|
||||
return parts.get(2).copied().unwrap_or("根目录").to_string();
|
||||
}
|
||||
if parts.len() > 1 {
|
||||
parts[0].to_string()
|
||||
} else {
|
||||
"根目录".into()
|
||||
}
|
||||
}
|
||||
|
||||
fn frontmatter_tags(bytes: &[u8]) -> Vec<String> {
|
||||
let Ok(body) = std::str::from_utf8(bytes) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let normalized = body.replace("\r\n", "\n");
|
||||
if !normalized.starts_with("---\n") {
|
||||
return Vec::new();
|
||||
}
|
||||
let Some(closing) = normalized[4..].find("\n---\n").map(|position| position + 4) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut tags = Vec::new();
|
||||
let mut reading_list = false;
|
||||
for line in normalized[4..closing].lines() {
|
||||
let trimmed = line.trim();
|
||||
if reading_list && trimmed.starts_with('-') {
|
||||
tags.push(
|
||||
trimmed
|
||||
.trim_start_matches('-')
|
||||
.trim()
|
||||
.trim_matches(['\'', '"'])
|
||||
.to_string(),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
reading_list = false;
|
||||
let Some((key, value)) = trimmed.split_once(':') else {
|
||||
continue;
|
||||
};
|
||||
if key.trim() != "tags" {
|
||||
continue;
|
||||
}
|
||||
let value = value.trim();
|
||||
if value.is_empty() {
|
||||
reading_list = true;
|
||||
continue;
|
||||
}
|
||||
let inline = value.trim_matches(['[', ']']);
|
||||
tags.extend(
|
||||
inline
|
||||
.split(',')
|
||||
.map(|tag| tag.trim().trim_matches(['\'', '"']).to_string()),
|
||||
);
|
||||
}
|
||||
normalize_tags(tags).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn replace_organization_file(target: &Path, bytes: &[u8]) -> Result<(), String> {
|
||||
let parent = target
|
||||
.parent()
|
||||
.ok_or_else(|| "HOLOLAKE_KNOWLEDGE_ORGANIZATION_PATH_INVALID".to_string())?;
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_ORGANIZATION_WRITE_FAILED: {error}"))?;
|
||||
let temporary = parent.join(format!("organization-{}.tmp", Uuid::new_v4()));
|
||||
let mut file = private_file_options()
|
||||
.open(&temporary)
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_ORGANIZATION_WRITE_FAILED: {error}"))?;
|
||||
file.write_all(bytes)
|
||||
.and_then(|_| file.sync_all())
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_ORGANIZATION_WRITE_FAILED: {error}"))?;
|
||||
if !target.exists() {
|
||||
return fs::rename(&temporary, target)
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_ORGANIZATION_WRITE_FAILED: {error}"));
|
||||
}
|
||||
let backup = parent.join(format!("organization-{}.backup", Uuid::new_v4()));
|
||||
fs::rename(target, &backup)
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_ORGANIZATION_WRITE_FAILED: {error}"))?;
|
||||
if let Err(error) = fs::rename(&temporary, target) {
|
||||
let _ = fs::rename(&backup, target);
|
||||
let _ = fs::remove_file(&temporary);
|
||||
return Err(format!(
|
||||
"HOLOLAKE_KNOWLEDGE_ORGANIZATION_WRITE_FAILED: {error}"
|
||||
));
|
||||
}
|
||||
let _ = fs::remove_file(backup);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_organization_index(root: &Path, index: &KnowledgeOrganizationIndex) -> Result<(), String> {
|
||||
let mut bytes = serde_json::to_vec_pretty(index)
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_ORGANIZATION_SERIALIZE_FAILED: {error}"))?;
|
||||
bytes.push(b'\n');
|
||||
replace_organization_file(&organization_index_path(root), &bytes)
|
||||
}
|
||||
|
||||
fn update_organization_at(
|
||||
native: &Path,
|
||||
legacy: Option<&Path>,
|
||||
input: UpdateKnowledgeOrganizationInput,
|
||||
) -> Result<KnowledgeOrganizationUpdateResult, String> {
|
||||
if input.source != "native" {
|
||||
return Err("HOLOLAKE_KNOWLEDGE_ORGANIZATION_READ_ONLY_SOURCE".into());
|
||||
}
|
||||
let category = normalize_category(&input.category)?;
|
||||
let tags = normalize_tags(input.tags)?;
|
||||
let document_path = safe_document_path(&native.join("docs"), &input.path)?;
|
||||
let document_bytes = fs::read(&document_path)
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_DOCUMENT_UNAVAILABLE: {error}"))?;
|
||||
if sha256_hex(&document_bytes) != input.expected_content_sha256 {
|
||||
return Err("HOLOLAKE_KNOWLEDGE_ORGANIZATION_CONFLICT".into());
|
||||
}
|
||||
let (mut index, _) = load_organization_index(native)?;
|
||||
let unchanged = index
|
||||
.documents
|
||||
.get(&input.path)
|
||||
.map(|entry| entry.category == category && entry.tags == tags)
|
||||
.unwrap_or(false);
|
||||
if !unchanged {
|
||||
let updated_at_unix_ms = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|duration| duration.as_millis())
|
||||
.unwrap_or(0);
|
||||
index.documents.insert(
|
||||
input.path.clone(),
|
||||
KnowledgeOrganizationEntry {
|
||||
category,
|
||||
tags,
|
||||
updated_at_unix_ms,
|
||||
},
|
||||
);
|
||||
write_organization_index(native, &index)?;
|
||||
git(
|
||||
native,
|
||||
&["add", "--", ".hololake/organization-v1.json"],
|
||||
"ORGANIZATION_ADD",
|
||||
)?;
|
||||
git(
|
||||
native,
|
||||
&[
|
||||
"commit",
|
||||
"-m",
|
||||
&format!("knowledge: organize {}", input.path),
|
||||
],
|
||||
"ORGANIZATION_COMMIT",
|
||||
)?;
|
||||
}
|
||||
let git_commit = git(native, &["rev-parse", "HEAD"], "ORGANIZATION_READBACK")?
|
||||
.trim()
|
||||
.to_string();
|
||||
Ok(KnowledgeOrganizationUpdateResult {
|
||||
schema: "hololake.knowledge-organization-update/v1",
|
||||
state: if unchanged { "UNCHANGED" } else { "SAVED" },
|
||||
git_commit,
|
||||
snapshot: snapshot_at(native, legacy)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn snapshot_at(native: &Path, legacy: Option<&Path>) -> Result<KnowledgeSnapshot, String> {
|
||||
let (organization, organization_revision) = load_organization_index(native)?;
|
||||
let mut raw_documents = Vec::new();
|
||||
collect_documents(
|
||||
&native.join("docs"),
|
||||
&native.join("docs"),
|
||||
"native",
|
||||
Some(&organization),
|
||||
&mut raw_documents,
|
||||
)?;
|
||||
if let Some(root) = legacy {
|
||||
|
|
@ -517,6 +929,7 @@ fn snapshot_at(native: &Path, legacy: Option<&Path>) -> Result<KnowledgeSnapshot
|
|||
&root.join("docs"),
|
||||
&root.join("docs"),
|
||||
"legacy",
|
||||
None,
|
||||
&mut raw_documents,
|
||||
)?;
|
||||
}
|
||||
|
|
@ -539,6 +952,53 @@ fn snapshot_at(native: &Path, legacy: Option<&Path>) -> Result<KnowledgeSnapshot
|
|||
let truncated = raw_document_count >= MAX_TREE_DOCUMENTS;
|
||||
documents.truncate(MAX_TREE_DOCUMENTS);
|
||||
let unique_document_count = documents.len();
|
||||
let organized_document_count = documents
|
||||
.iter()
|
||||
.filter(|document| document.category_explicit)
|
||||
.count();
|
||||
let mut category_counts = BTreeMap::<String, (usize, usize)>::new();
|
||||
let mut tag_counts = BTreeMap::<String, usize>::new();
|
||||
for document in &documents {
|
||||
let category = category_counts
|
||||
.entry(document.category.clone())
|
||||
.or_default();
|
||||
category.0 += 1;
|
||||
if document.category_explicit {
|
||||
category.1 += 1;
|
||||
}
|
||||
for tag in &document.tags {
|
||||
*tag_counts.entry(tag.clone()).or_default() += 1;
|
||||
}
|
||||
}
|
||||
let mut categories = category_counts
|
||||
.into_iter()
|
||||
.map(
|
||||
|(name, (document_count, explicitly_organized_count))| KnowledgeCategorySummary {
|
||||
name,
|
||||
document_count,
|
||||
explicitly_organized_count,
|
||||
},
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
categories.sort_by(|left, right| {
|
||||
right
|
||||
.document_count
|
||||
.cmp(&left.document_count)
|
||||
.then_with(|| left.name.cmp(&right.name))
|
||||
});
|
||||
let mut tags = tag_counts
|
||||
.into_iter()
|
||||
.map(|(name, document_count)| KnowledgeTagSummary {
|
||||
name,
|
||||
document_count,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
tags.sort_by(|left, right| {
|
||||
right
|
||||
.document_count
|
||||
.cmp(&left.document_count)
|
||||
.then_with(|| left.name.cmp(&right.name))
|
||||
});
|
||||
Ok(KnowledgeSnapshot {
|
||||
schema: SNAPSHOT_SCHEMA,
|
||||
state: "READY",
|
||||
|
|
@ -549,6 +1009,10 @@ fn snapshot_at(native: &Path, legacy: Option<&Path>) -> Result<KnowledgeSnapshot
|
|||
raw_document_count,
|
||||
unique_document_count,
|
||||
duplicate_document_count: raw_document_count.saturating_sub(unique_document_count),
|
||||
organized_document_count,
|
||||
categories,
|
||||
tags,
|
||||
organization_revision,
|
||||
truncated,
|
||||
})
|
||||
}
|
||||
|
|
@ -557,6 +1021,7 @@ fn collect_documents(
|
|||
root: &Path,
|
||||
current: &Path,
|
||||
source: &'static str,
|
||||
organization: Option<&KnowledgeOrganizationIndex>,
|
||||
output: &mut Vec<KnowledgeDocumentSummary>,
|
||||
) -> Result<(), String> {
|
||||
if output.len() >= MAX_TREE_DOCUMENTS {
|
||||
|
|
@ -581,7 +1046,7 @@ fn collect_documents(
|
|||
let name = entry.file_name();
|
||||
if kind.is_dir() {
|
||||
if name != OsStr::new(".git") && name != OsStr::new(".hololake") {
|
||||
collect_documents(root, &path, source, output)?;
|
||||
collect_documents(root, &path, source, organization, output)?;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
|
@ -594,9 +1059,22 @@ fn collect_documents(
|
|||
if metadata.len() > MAX_READ_BYTES {
|
||||
continue;
|
||||
}
|
||||
let relative = relative_posix(root, &path)?;
|
||||
let bytes = fs::read(&path)
|
||||
.map_err(|error| format!("HOLOLAKE_KNOWLEDGE_DOCUMENT_UNAVAILABLE: {error}"))?;
|
||||
let relative = relative_posix(root, &path)?;
|
||||
let frontmatter_tags = frontmatter_tags(&bytes);
|
||||
let indexed = organization.and_then(|index| index.documents.get(&relative));
|
||||
let category = indexed
|
||||
.map(|entry| entry.category.clone())
|
||||
.filter(|category| !category.is_empty())
|
||||
.unwrap_or_else(|| derived_category(&relative));
|
||||
let mut tags = indexed.map(|entry| entry.tags.clone()).unwrap_or_default();
|
||||
for tag in frontmatter_tags {
|
||||
if !tags.contains(&tag) {
|
||||
tags.push(tag);
|
||||
}
|
||||
}
|
||||
tags.sort();
|
||||
output.push(KnowledgeDocumentSummary {
|
||||
source,
|
||||
path: relative,
|
||||
|
|
@ -605,6 +1083,9 @@ fn collect_documents(
|
|||
size_bytes: metadata.len(),
|
||||
content_sha256: sha256_hex(&bytes),
|
||||
duplicate_count: 0,
|
||||
category,
|
||||
category_explicit: indexed.is_some(),
|
||||
tags,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
|
|
@ -990,7 +1471,9 @@ fn strip_notion_links(line: &str) -> String {
|
|||
let text_end = i + 1 + close;
|
||||
if text_end + 1 < bytes.len() && bytes[text_end + 1] == '(' {
|
||||
if let Some(paren_end) = bytes[text_end + 2..].iter().position(|c| *c == ')') {
|
||||
let href: String = bytes[text_end + 2..text_end + 2 + paren_end].iter().collect();
|
||||
let href: String = bytes[text_end + 2..text_end + 2 + paren_end]
|
||||
.iter()
|
||||
.collect();
|
||||
if href.contains("notion.so") || href.contains("notion.site") {
|
||||
out.extend(bytes[i + 1..text_end].iter());
|
||||
i = text_end + 2 + paren_end + 1;
|
||||
|
|
@ -1376,4 +1859,130 @@ mod tests {
|
|||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].title, "星湖");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_builds_unified_categories_and_frontmatter_tags() {
|
||||
let native = tempdir().unwrap();
|
||||
ensure_native_knowledge_root(native.path()).unwrap();
|
||||
fs::create_dir_all(native.path().join("docs/导入/旧库/课程")).unwrap();
|
||||
fs::write(
|
||||
native.path().join("docs/导入/旧库/课程/第一课.md"),
|
||||
"---\ntags: [教学, 重点]\n---\n# 第一课\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let snapshot = snapshot_at(native.path(), None).unwrap();
|
||||
|
||||
assert_eq!(snapshot.documents[0].category, "课程");
|
||||
assert_eq!(snapshot.documents[0].tags, vec!["教学", "重点"]);
|
||||
assert_eq!(snapshot.categories[0].name, "课程");
|
||||
assert_eq!(snapshot.tags.len(), 2);
|
||||
assert_eq!(snapshot.organized_document_count, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_organization_is_revision_locked_git_backed_and_does_not_rewrite_body() {
|
||||
let native = tempdir().unwrap();
|
||||
ensure_native_knowledge_root(native.path()).unwrap();
|
||||
let body = "---\ntags: [原有]\n---\n# 页面\n\n正文不应被整理操作改写。\n";
|
||||
fs::write(native.path().join("docs/page.md"), body).unwrap();
|
||||
git(native.path(), &["add", "docs/page.md"], "TEST_ADD").unwrap();
|
||||
git(native.path(), &["commit", "-m", "initial"], "TEST_COMMIT").unwrap();
|
||||
let hash = sha256_hex(body.as_bytes());
|
||||
|
||||
let result = update_organization_at(
|
||||
native.path(),
|
||||
None,
|
||||
UpdateKnowledgeOrganizationInput {
|
||||
source: "native".into(),
|
||||
path: "page.md".into(),
|
||||
expected_content_sha256: hash.clone(),
|
||||
category: "教学 / 课程".into(),
|
||||
tags: vec!["待复盘".into(), "重点".into()],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result.state, "SAVED");
|
||||
assert_eq!(result.git_commit.len(), 40);
|
||||
assert_eq!(
|
||||
fs::read_to_string(native.path().join("docs/page.md")).unwrap(),
|
||||
body
|
||||
);
|
||||
let document = &result.snapshot.documents[0];
|
||||
assert_eq!(document.category, "教学 / 课程");
|
||||
assert!(document.category_explicit);
|
||||
assert_eq!(document.tags, vec!["原有", "待复盘", "重点"]);
|
||||
assert_eq!(result.snapshot.organized_document_count, 1);
|
||||
|
||||
let stale = update_organization_at(
|
||||
native.path(),
|
||||
None,
|
||||
UpdateKnowledgeOrganizationInput {
|
||||
source: "native".into(),
|
||||
path: "page.md".into(),
|
||||
expected_content_sha256: sha256_hex(b"stale"),
|
||||
category: "其他".into(),
|
||||
tags: vec![],
|
||||
},
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(stale.contains("ORGANIZATION_CONFLICT"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn organized_page_archive_moves_into_recovery_cleans_index_and_commits() {
|
||||
let native = tempdir().unwrap();
|
||||
ensure_native_knowledge_root(native.path()).unwrap();
|
||||
let body = "# 待归档页面\n";
|
||||
let target = native.path().join("docs/page.md");
|
||||
fs::write(&target, body).unwrap();
|
||||
git(native.path(), &["add", "docs/page.md"], "TEST_ADD").unwrap();
|
||||
git(native.path(), &["commit", "-m", "initial"], "TEST_COMMIT").unwrap();
|
||||
update_organization_at(
|
||||
native.path(),
|
||||
None,
|
||||
UpdateKnowledgeOrganizationInput {
|
||||
source: "native".into(),
|
||||
path: "page.md".into(),
|
||||
expected_content_sha256: sha256_hex(body.as_bytes()),
|
||||
category: "验收 / 归档".into(),
|
||||
tags: vec!["可恢复".into()],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let destination = archive_native_entry(native.path(), &target, "page.md", false).unwrap();
|
||||
|
||||
assert!(!target.exists());
|
||||
assert!(destination.is_file());
|
||||
assert!(destination.starts_with(native.path().join(".trash")));
|
||||
let (organization, _) = load_organization_index(native.path()).unwrap();
|
||||
assert!(!organization.documents.contains_key("page.md"));
|
||||
assert_eq!(
|
||||
snapshot_at(native.path(), None)
|
||||
.unwrap()
|
||||
.unique_document_count,
|
||||
0
|
||||
);
|
||||
assert!(
|
||||
git(native.path(), &["status", "--porcelain"], "TEST_STATUS")
|
||||
.unwrap()
|
||||
.trim()
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn organization_rejects_unbounded_or_cross_platform_invalid_labels() {
|
||||
assert!(normalize_category("课程/章节/单元/课时/过深").is_err());
|
||||
assert!(normalize_category("课程:非法").is_err());
|
||||
assert!(normalize_tags(vec!["a".repeat(MAX_TAG_CHARS + 1)]).is_err());
|
||||
assert!(normalize_tags(
|
||||
(0..=MAX_TAGS_PER_DOCUMENT)
|
||||
.map(|index| index.to_string())
|
||||
.collect()
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
mod authenticated_storage;
|
||||
mod channel_growth;
|
||||
mod circular_lake_membrane;
|
||||
mod code_channel;
|
||||
mod code_repo_login;
|
||||
mod direct_local_broker;
|
||||
mod direct_local_session;
|
||||
mod dynamic_capability_routing;
|
||||
mod education_translation;
|
||||
mod education_workspace;
|
||||
mod enterprise_work_channel;
|
||||
mod glp_envelope;
|
||||
mod gls_bootstrap_compiler;
|
||||
|
|
@ -13,6 +16,8 @@ mod gls_protocol_runtime;
|
|||
mod home_status;
|
||||
mod knowledge_base;
|
||||
mod local_development_bridge;
|
||||
mod native_composition;
|
||||
mod persona_channel_body;
|
||||
mod persona_time_authority;
|
||||
mod personal_channel;
|
||||
mod pncc_receipt_projection;
|
||||
|
|
@ -59,14 +64,42 @@ pub fn run() {
|
|||
personal_channel::initialize_personal_channel,
|
||||
personal_channel::create_personal_channel_task,
|
||||
personal_channel::transition_personal_channel_task,
|
||||
channel_growth::get_channel_growth_snapshot,
|
||||
channel_growth::record_channel_growth_event,
|
||||
channel_growth::update_channel_growth_sharing,
|
||||
persona_time_authority::issue_persona_time_ticket,
|
||||
persona_time_authority::start_persona_time_authority,
|
||||
persona_time_authority::get_beijing_time_coordinate,
|
||||
persona_time_authority::get_guanghu_era_timeline,
|
||||
persona_channel_body::get_persona_channel_body,
|
||||
persona_channel_body::register_trial_persona,
|
||||
persona_channel_body::delete_trial_persona,
|
||||
persona_channel_body::accept_persona_language_contract,
|
||||
persona_channel_body::append_persona_language,
|
||||
native_composition::get_native_composition_module_registry,
|
||||
native_composition::execute_knowledge_native_composition,
|
||||
education_workspace::get_education_workspace_snapshot,
|
||||
education_workspace::create_education_document,
|
||||
education_workspace::read_education_document,
|
||||
education_workspace::save_education_document,
|
||||
education_workspace::archive_education_document,
|
||||
education_workspace::create_education_table,
|
||||
education_workspace::read_education_table,
|
||||
education_workspace::save_education_table,
|
||||
education_workspace::archive_education_table,
|
||||
education_translation::import_education_tables_from_dialog,
|
||||
education_translation::export_education_table_to_dialog,
|
||||
education_translation::get_education_recognition_capability,
|
||||
education_workspace::create_education_automation_rule,
|
||||
education_workspace::save_education_automation_rule,
|
||||
education_workspace::archive_education_automation_rule,
|
||||
education_workspace::preview_education_automation_rule,
|
||||
education_workspace::execute_education_automation_rule,
|
||||
knowledge_base::get_knowledge_snapshot,
|
||||
knowledge_base::read_knowledge_document,
|
||||
knowledge_base::search_knowledge,
|
||||
knowledge_base::save_knowledge_document,
|
||||
knowledge_base::update_knowledge_organization,
|
||||
knowledge_base::select_and_import_knowledge_folder,
|
||||
knowledge_base::export_knowledge_document,
|
||||
knowledge_base::create_knowledge_document,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,833 @@
|
|||
//! HoloLake 原生组合执行内核。
|
||||
//!
|
||||
//! 外来文件格式不进入这里。内核只接受登记过的模块、受限配方和当前登录账号的
|
||||
//! HoloLake 原生对象;人类投影只是同一执行结果的不同视图,没有源数据所有权。
|
||||
|
||||
use ring::digest::{digest, SHA256};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, BTreeSet, VecDeque};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tauri::AppHandle;
|
||||
use uuid::Uuid;
|
||||
|
||||
const REGISTRY_SCHEMA: &str = "hololake.composition-module-registry/v1";
|
||||
const OBJECT_SCHEMA: &str = "hololake.native-object/v1";
|
||||
const RECIPE_SCHEMA: &str = "hololake.composition-recipe/v1";
|
||||
const PROJECTION_SCHEMA: &str = "hololake.human-projection/v1";
|
||||
const MAX_ROWS: usize = 5_000;
|
||||
const MAX_VIEWS: usize = 5;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum CompositionDimension {
|
||||
Source,
|
||||
TopLevelFolder,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum CompositionMeasure {
|
||||
DocumentCount,
|
||||
TotalBytes,
|
||||
DuplicateCount,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum ProjectionView {
|
||||
Dashboard,
|
||||
Comparison,
|
||||
VerticalBar,
|
||||
Classification,
|
||||
Table,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct ExecuteCompositionInput {
|
||||
pub dimension: CompositionDimension,
|
||||
pub measure: CompositionMeasure,
|
||||
pub views: Vec<ProjectionView>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ModuleDescriptor {
|
||||
pub module_id: &'static str,
|
||||
pub display_name: &'static str,
|
||||
pub kind: &'static str,
|
||||
pub input_schema: Option<&'static str>,
|
||||
pub output_schema: &'static str,
|
||||
pub deterministic: bool,
|
||||
pub authority: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ModuleRegistry {
|
||||
pub schema: &'static str,
|
||||
pub state: &'static str,
|
||||
pub modules: Vec<ModuleDescriptor>,
|
||||
pub arbitrary_script_allowed: bool,
|
||||
pub unregistered_module_allowed: bool,
|
||||
pub direct_projection_write_allowed: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NativeColumn {
|
||||
pub column_id: &'static str,
|
||||
pub title: &'static str,
|
||||
pub value_type: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NativeKnowledgeRow {
|
||||
pub row_id: String,
|
||||
pub source: String,
|
||||
pub path: String,
|
||||
pub title: String,
|
||||
pub top_level_folder: String,
|
||||
pub size_bytes: u64,
|
||||
pub duplicate_count: usize,
|
||||
pub updated_at_unix_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NativeObject {
|
||||
pub schema: &'static str,
|
||||
pub object_id: String,
|
||||
pub title: &'static str,
|
||||
pub columns: Vec<NativeColumn>,
|
||||
pub rows: Vec<NativeKnowledgeRow>,
|
||||
pub row_count: usize,
|
||||
pub truncated: bool,
|
||||
pub source_receipt: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CompositionNode {
|
||||
pub node_id: String,
|
||||
pub module_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CompositionEdge {
|
||||
pub from: String,
|
||||
pub to: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CompositionRecipe {
|
||||
pub schema: &'static str,
|
||||
pub recipe_id: &'static str,
|
||||
pub title: &'static str,
|
||||
pub nodes: Vec<CompositionNode>,
|
||||
pub edges: Vec<CompositionEdge>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProjectionGroup {
|
||||
pub key: String,
|
||||
pub label: String,
|
||||
pub document_count: usize,
|
||||
pub total_bytes: u64,
|
||||
pub duplicate_count: usize,
|
||||
pub measure_value: u64,
|
||||
pub share: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProjectionMetrics {
|
||||
pub raw_document_count: usize,
|
||||
pub unique_document_count: usize,
|
||||
pub duplicate_document_count: usize,
|
||||
pub total_bytes: u64,
|
||||
pub group_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NativeCompositionProjection {
|
||||
pub schema: &'static str,
|
||||
pub state: &'static str,
|
||||
pub execution_id: String,
|
||||
pub executed_at_unix_ms: u64,
|
||||
pub source_is_real_account_data: bool,
|
||||
pub read_only: bool,
|
||||
pub dimension: CompositionDimension,
|
||||
pub measure: CompositionMeasure,
|
||||
pub views: Vec<ProjectionView>,
|
||||
pub native_object: NativeObject,
|
||||
pub groups: Vec<ProjectionGroup>,
|
||||
pub metrics: ProjectionMetrics,
|
||||
pub recipe: CompositionRecipe,
|
||||
pub data_sha256: String,
|
||||
pub receipt_sha256: String,
|
||||
}
|
||||
|
||||
fn registry() -> ModuleRegistry {
|
||||
let modules = vec![
|
||||
descriptor(
|
||||
"HLC-SOURCE-KNOWLEDGE-CATALOG",
|
||||
"知识目录数据源",
|
||||
"SOURCE",
|
||||
None,
|
||||
OBJECT_SCHEMA,
|
||||
),
|
||||
descriptor(
|
||||
"HLC-CLASSIFY-SOURCE",
|
||||
"按来源分类",
|
||||
"TRANSFORM",
|
||||
Some(OBJECT_SCHEMA),
|
||||
"hololake.classified-object/v1",
|
||||
),
|
||||
descriptor(
|
||||
"HLC-CLASSIFY-TOP-FOLDER",
|
||||
"按一级目录分类",
|
||||
"TRANSFORM",
|
||||
Some(OBJECT_SCHEMA),
|
||||
"hololake.classified-object/v1",
|
||||
),
|
||||
descriptor(
|
||||
"HLC-AGGREGATE-DOCUMENT-COUNT",
|
||||
"统计文档数",
|
||||
"TRANSFORM",
|
||||
Some("hololake.classified-object/v1"),
|
||||
"hololake.composition-result/v1",
|
||||
),
|
||||
descriptor(
|
||||
"HLC-AGGREGATE-TOTAL-BYTES",
|
||||
"统计数据量",
|
||||
"TRANSFORM",
|
||||
Some("hololake.classified-object/v1"),
|
||||
"hololake.composition-result/v1",
|
||||
),
|
||||
descriptor(
|
||||
"HLC-AGGREGATE-DUPLICATE-COUNT",
|
||||
"统计重复数",
|
||||
"TRANSFORM",
|
||||
Some("hololake.classified-object/v1"),
|
||||
"hololake.composition-result/v1",
|
||||
),
|
||||
descriptor(
|
||||
"HLC-PROJECT-DASHBOARD",
|
||||
"仪表盘投影",
|
||||
"PROJECTION",
|
||||
Some("hololake.composition-result/v1"),
|
||||
PROJECTION_SCHEMA,
|
||||
),
|
||||
descriptor(
|
||||
"HLC-PROJECT-COMPARISON",
|
||||
"对比投影",
|
||||
"PROJECTION",
|
||||
Some("hololake.composition-result/v1"),
|
||||
PROJECTION_SCHEMA,
|
||||
),
|
||||
descriptor(
|
||||
"HLC-PROJECT-VERTICAL-BAR",
|
||||
"柱状图投影",
|
||||
"PROJECTION",
|
||||
Some("hololake.composition-result/v1"),
|
||||
PROJECTION_SCHEMA,
|
||||
),
|
||||
descriptor(
|
||||
"HLC-PROJECT-CLASSIFICATION",
|
||||
"分类投影",
|
||||
"PROJECTION",
|
||||
Some("hololake.composition-result/v1"),
|
||||
PROJECTION_SCHEMA,
|
||||
),
|
||||
descriptor(
|
||||
"HLC-PROJECT-TABLE",
|
||||
"表格投影",
|
||||
"PROJECTION",
|
||||
Some("hololake.composition-result/v1"),
|
||||
PROJECTION_SCHEMA,
|
||||
),
|
||||
];
|
||||
ModuleRegistry {
|
||||
schema: REGISTRY_SCHEMA,
|
||||
state: "READY",
|
||||
modules,
|
||||
arbitrary_script_allowed: false,
|
||||
unregistered_module_allowed: false,
|
||||
direct_projection_write_allowed: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn descriptor(
|
||||
module_id: &'static str,
|
||||
display_name: &'static str,
|
||||
kind: &'static str,
|
||||
input_schema: Option<&'static str>,
|
||||
output_schema: &'static str,
|
||||
) -> ModuleDescriptor {
|
||||
ModuleDescriptor {
|
||||
module_id,
|
||||
display_name,
|
||||
kind,
|
||||
input_schema,
|
||||
output_schema,
|
||||
deterministic: true,
|
||||
authority: "CURRENT_AUTHENTICATED_ACCOUNT_READ_ONLY",
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_native_composition_module_registry() -> ModuleRegistry {
|
||||
registry()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn execute_knowledge_native_composition(
|
||||
app: AppHandle,
|
||||
input: ExecuteCompositionInput,
|
||||
) -> Result<NativeCompositionProjection, String> {
|
||||
validate_input(&input)?;
|
||||
let snapshot = tauri::async_runtime::spawn_blocking(move || {
|
||||
crate::knowledge_base::snapshot_for_native_composition(&app)
|
||||
})
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_COMPOSITION_JOIN_FAILED: {error}"))??;
|
||||
execute_snapshot(snapshot, input)
|
||||
}
|
||||
|
||||
fn validate_input(input: &ExecuteCompositionInput) -> Result<(), String> {
|
||||
if input.views.is_empty() || input.views.len() > MAX_VIEWS {
|
||||
return Err("HOLOLAKE_COMPOSITION_VIEW_COUNT_INVALID".into());
|
||||
}
|
||||
let unique = input.views.iter().copied().collect::<BTreeSet<_>>();
|
||||
if unique.len() != input.views.len() {
|
||||
return Err("HOLOLAKE_COMPOSITION_DUPLICATE_VIEW".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn execute_snapshot(
|
||||
snapshot: crate::knowledge_base::KnowledgeSnapshot,
|
||||
input: ExecuteCompositionInput,
|
||||
) -> Result<NativeCompositionProjection, String> {
|
||||
if snapshot.documents.len() > MAX_ROWS {
|
||||
return Err("HOLOLAKE_COMPOSITION_ROW_LIMIT_EXCEEDED".into());
|
||||
}
|
||||
let rows = snapshot
|
||||
.documents
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, document)| NativeKnowledgeRow {
|
||||
row_id: format!("knowledge-row-{}", index + 1),
|
||||
source: document.source.to_string(),
|
||||
path: document.path.clone(),
|
||||
title: document.title.clone(),
|
||||
top_level_folder: document.category.clone(),
|
||||
size_bytes: document.size_bytes,
|
||||
duplicate_count: document.duplicate_count,
|
||||
updated_at_unix_ms: document.updated_at_unix_ms.min(u64::MAX as u128) as u64,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let data_sha256 = sha256_json(&rows)?;
|
||||
let native_object = NativeObject {
|
||||
schema: OBJECT_SCHEMA,
|
||||
object_id: format!("knowledge-catalog-{}", &data_sha256[..16]),
|
||||
title: "当前账号知识目录",
|
||||
columns: native_columns(),
|
||||
row_count: rows.len(),
|
||||
rows,
|
||||
truncated: snapshot.truncated,
|
||||
source_receipt: "CURRENT_AUTHENTICATED_ACCOUNT_KNOWLEDGE_SNAPSHOT",
|
||||
};
|
||||
let mut aggregates = BTreeMap::<String, (usize, u64, usize)>::new();
|
||||
for row in &native_object.rows {
|
||||
let key = match input.dimension {
|
||||
CompositionDimension::Source => source_label(&row.source),
|
||||
CompositionDimension::TopLevelFolder => row.top_level_folder.clone(),
|
||||
};
|
||||
let entry = aggregates.entry(key).or_default();
|
||||
entry.0 += 1;
|
||||
entry.1 = entry.1.saturating_add(row.size_bytes);
|
||||
entry.2 = entry.2.saturating_add(row.duplicate_count);
|
||||
}
|
||||
let total_measure = aggregates
|
||||
.values()
|
||||
.map(|value| measure_value(*value, input.measure))
|
||||
.sum::<u64>();
|
||||
let mut groups = aggregates
|
||||
.into_iter()
|
||||
.map(|(label, value)| ProjectionGroup {
|
||||
key: stable_key(&label),
|
||||
label,
|
||||
document_count: value.0,
|
||||
total_bytes: value.1,
|
||||
duplicate_count: value.2,
|
||||
measure_value: measure_value(value, input.measure),
|
||||
share: if total_measure == 0 {
|
||||
0.0
|
||||
} else {
|
||||
measure_value(value, input.measure) as f64 / total_measure as f64
|
||||
},
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
groups.sort_by(|left, right| {
|
||||
right
|
||||
.measure_value
|
||||
.cmp(&left.measure_value)
|
||||
.then_with(|| left.label.cmp(&right.label))
|
||||
});
|
||||
let recipe = compile_recipe(&input);
|
||||
validate_recipe(&recipe, ®istry())?;
|
||||
let metrics = ProjectionMetrics {
|
||||
raw_document_count: snapshot.raw_document_count,
|
||||
unique_document_count: snapshot.unique_document_count,
|
||||
duplicate_document_count: snapshot.duplicate_document_count,
|
||||
total_bytes: native_object.rows.iter().map(|row| row.size_bytes).sum(),
|
||||
group_count: groups.len(),
|
||||
};
|
||||
let executed_at_unix_ms = now_unix_ms();
|
||||
let receipt_material = serde_json::to_vec(&(
|
||||
&data_sha256,
|
||||
&recipe.nodes,
|
||||
&recipe.edges,
|
||||
&groups,
|
||||
executed_at_unix_ms,
|
||||
))
|
||||
.map_err(|error| format!("HOLOLAKE_COMPOSITION_RECEIPT_SERIALIZE_FAILED: {error}"))?;
|
||||
Ok(NativeCompositionProjection {
|
||||
schema: PROJECTION_SCHEMA,
|
||||
state: "EXECUTED",
|
||||
execution_id: format!("HLC-EXEC-{}", Uuid::new_v4()),
|
||||
executed_at_unix_ms,
|
||||
source_is_real_account_data: true,
|
||||
read_only: true,
|
||||
dimension: input.dimension,
|
||||
measure: input.measure,
|
||||
views: input.views,
|
||||
native_object,
|
||||
groups,
|
||||
metrics,
|
||||
recipe,
|
||||
data_sha256,
|
||||
receipt_sha256: sha256(&receipt_material),
|
||||
})
|
||||
}
|
||||
|
||||
fn native_columns() -> Vec<NativeColumn> {
|
||||
vec![
|
||||
NativeColumn {
|
||||
column_id: "title",
|
||||
title: "标题",
|
||||
value_type: "TEXT",
|
||||
},
|
||||
NativeColumn {
|
||||
column_id: "source",
|
||||
title: "来源",
|
||||
value_type: "TEXT",
|
||||
},
|
||||
NativeColumn {
|
||||
column_id: "topLevelFolder",
|
||||
title: "一级分类",
|
||||
value_type: "TEXT",
|
||||
},
|
||||
NativeColumn {
|
||||
column_id: "sizeBytes",
|
||||
title: "数据量",
|
||||
value_type: "INTEGER",
|
||||
},
|
||||
NativeColumn {
|
||||
column_id: "duplicateCount",
|
||||
title: "重复数",
|
||||
value_type: "INTEGER",
|
||||
},
|
||||
NativeColumn {
|
||||
column_id: "updatedAtUnixMs",
|
||||
title: "更新时间",
|
||||
value_type: "TIME",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn compile_recipe(input: &ExecuteCompositionInput) -> CompositionRecipe {
|
||||
let classify = match input.dimension {
|
||||
CompositionDimension::Source => "HLC-CLASSIFY-SOURCE",
|
||||
CompositionDimension::TopLevelFolder => "HLC-CLASSIFY-TOP-FOLDER",
|
||||
};
|
||||
let aggregate = match input.measure {
|
||||
CompositionMeasure::DocumentCount => "HLC-AGGREGATE-DOCUMENT-COUNT",
|
||||
CompositionMeasure::TotalBytes => "HLC-AGGREGATE-TOTAL-BYTES",
|
||||
CompositionMeasure::DuplicateCount => "HLC-AGGREGATE-DUPLICATE-COUNT",
|
||||
};
|
||||
let mut nodes = vec![
|
||||
CompositionNode {
|
||||
node_id: "source".into(),
|
||||
module_id: "HLC-SOURCE-KNOWLEDGE-CATALOG".into(),
|
||||
},
|
||||
CompositionNode {
|
||||
node_id: "classify".into(),
|
||||
module_id: classify.into(),
|
||||
},
|
||||
CompositionNode {
|
||||
node_id: "aggregate".into(),
|
||||
module_id: aggregate.into(),
|
||||
},
|
||||
];
|
||||
let mut edges = vec![
|
||||
CompositionEdge {
|
||||
from: "source".into(),
|
||||
to: "classify".into(),
|
||||
},
|
||||
CompositionEdge {
|
||||
from: "classify".into(),
|
||||
to: "aggregate".into(),
|
||||
},
|
||||
];
|
||||
for (index, view) in input.views.iter().enumerate() {
|
||||
let node_id = format!("projection-{}", index + 1);
|
||||
nodes.push(CompositionNode {
|
||||
node_id: node_id.clone(),
|
||||
module_id: view_module_id(*view).into(),
|
||||
});
|
||||
edges.push(CompositionEdge {
|
||||
from: "aggregate".into(),
|
||||
to: node_id,
|
||||
});
|
||||
}
|
||||
CompositionRecipe {
|
||||
schema: RECIPE_SCHEMA,
|
||||
recipe_id: "HLC-RECIPE-KNOWLEDGE-OVERVIEW-001",
|
||||
title: "知识空间结构组合",
|
||||
nodes,
|
||||
edges,
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_recipe(
|
||||
recipe: &CompositionRecipe,
|
||||
module_registry: &ModuleRegistry,
|
||||
) -> Result<(), String> {
|
||||
let descriptors = module_registry
|
||||
.modules
|
||||
.iter()
|
||||
.map(|item| (item.module_id, item))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
let nodes = recipe
|
||||
.nodes
|
||||
.iter()
|
||||
.map(|item| (item.node_id.as_str(), item))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
if nodes.len() != recipe.nodes.len() {
|
||||
return Err("HOLOLAKE_COMPOSITION_DUPLICATE_NODE".into());
|
||||
}
|
||||
for node in &recipe.nodes {
|
||||
if !descriptors.contains_key(node.module_id.as_str()) {
|
||||
return Err("HOLOLAKE_COMPOSITION_MODULE_NOT_REGISTERED".into());
|
||||
}
|
||||
}
|
||||
let mut indegree = recipe
|
||||
.nodes
|
||||
.iter()
|
||||
.map(|item| (item.node_id.as_str(), 0usize))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
let mut outgoing = BTreeMap::<&str, Vec<&str>>::new();
|
||||
for edge in &recipe.edges {
|
||||
let from = nodes
|
||||
.get(edge.from.as_str())
|
||||
.ok_or("HOLOLAKE_COMPOSITION_EDGE_NODE_UNKNOWN")?;
|
||||
let to = nodes
|
||||
.get(edge.to.as_str())
|
||||
.ok_or("HOLOLAKE_COMPOSITION_EDGE_NODE_UNKNOWN")?;
|
||||
let from_descriptor = descriptors
|
||||
.get(from.module_id.as_str())
|
||||
.ok_or("HOLOLAKE_COMPOSITION_MODULE_NOT_REGISTERED")?;
|
||||
let to_descriptor = descriptors
|
||||
.get(to.module_id.as_str())
|
||||
.ok_or("HOLOLAKE_COMPOSITION_MODULE_NOT_REGISTERED")?;
|
||||
if to_descriptor.input_schema != Some(from_descriptor.output_schema) {
|
||||
return Err("HOLOLAKE_COMPOSITION_PORT_TYPE_MISMATCH".into());
|
||||
}
|
||||
*indegree
|
||||
.get_mut(to.node_id.as_str())
|
||||
.ok_or("HOLOLAKE_COMPOSITION_EDGE_NODE_UNKNOWN")? += 1;
|
||||
outgoing
|
||||
.entry(from.node_id.as_str())
|
||||
.or_default()
|
||||
.push(to.node_id.as_str());
|
||||
}
|
||||
let mut queue = indegree
|
||||
.iter()
|
||||
.filter_map(|(id, degree)| (*degree == 0).then_some(*id))
|
||||
.collect::<VecDeque<_>>();
|
||||
let mut visited = 0usize;
|
||||
while let Some(node) = queue.pop_front() {
|
||||
visited += 1;
|
||||
for target in outgoing.get(node).into_iter().flatten() {
|
||||
let degree = indegree
|
||||
.get_mut(target)
|
||||
.ok_or("HOLOLAKE_COMPOSITION_EDGE_NODE_UNKNOWN")?;
|
||||
*degree -= 1;
|
||||
if *degree == 0 {
|
||||
queue.push_back(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
if visited != recipe.nodes.len() {
|
||||
return Err("HOLOLAKE_COMPOSITION_CYCLE_REJECTED".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn view_module_id(view: ProjectionView) -> &'static str {
|
||||
match view {
|
||||
ProjectionView::Dashboard => "HLC-PROJECT-DASHBOARD",
|
||||
ProjectionView::Comparison => "HLC-PROJECT-COMPARISON",
|
||||
ProjectionView::VerticalBar => "HLC-PROJECT-VERTICAL-BAR",
|
||||
ProjectionView::Classification => "HLC-PROJECT-CLASSIFICATION",
|
||||
ProjectionView::Table => "HLC-PROJECT-TABLE",
|
||||
}
|
||||
}
|
||||
|
||||
fn measure_value(value: (usize, u64, usize), measure: CompositionMeasure) -> u64 {
|
||||
match measure {
|
||||
CompositionMeasure::DocumentCount => value.0 as u64,
|
||||
CompositionMeasure::TotalBytes => value.1,
|
||||
CompositionMeasure::DuplicateCount => value.2 as u64,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn top_level_folder(path: &str) -> String {
|
||||
let normalized = path.replace('\\', "/");
|
||||
let parts = normalized
|
||||
.split('/')
|
||||
.filter(|part| !part.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
// “导入/导入批次名”是转译外壳,不是人类真正要比较的知识分类。
|
||||
let candidate = if parts.first() == Some(&"导入") {
|
||||
parts.get(2).copied().unwrap_or("根页面")
|
||||
} else {
|
||||
parts.first().copied().unwrap_or("未分类")
|
||||
};
|
||||
if candidate.ends_with(".md") || candidate.ends_with(".markdown") || candidate.ends_with(".txt")
|
||||
{
|
||||
"根目录".into()
|
||||
} else {
|
||||
candidate.into()
|
||||
}
|
||||
}
|
||||
|
||||
fn source_label(source: &str) -> String {
|
||||
match source {
|
||||
"native" => "光湖原生知识".into(),
|
||||
"legacy" => "待迁移知识".into(),
|
||||
other => other.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn stable_key(value: &str) -> String {
|
||||
sha256(value.as_bytes())[..16].to_string()
|
||||
}
|
||||
fn sha256_json<T: Serialize>(value: &T) -> Result<String, String> {
|
||||
serde_json::to_vec(value)
|
||||
.map(|bytes| sha256(&bytes))
|
||||
.map_err(|error| format!("HOLOLAKE_COMPOSITION_DATA_SERIALIZE_FAILED: {error}"))
|
||||
}
|
||||
fn sha256(bytes: &[u8]) -> String {
|
||||
digest(&SHA256, bytes)
|
||||
.as_ref()
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect()
|
||||
}
|
||||
fn now_unix_ms() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|value| value.as_millis().min(u64::MAX as u128) as u64)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::knowledge_base::{KnowledgeDocumentSummary, KnowledgeSnapshot};
|
||||
|
||||
fn snapshot() -> KnowledgeSnapshot {
|
||||
KnowledgeSnapshot {
|
||||
schema: "hololake.native-knowledge-base/v1",
|
||||
state: "READY",
|
||||
native_root: "/private/account".into(),
|
||||
legacy_available: false,
|
||||
legacy_root: None,
|
||||
documents: vec![
|
||||
KnowledgeDocumentSummary {
|
||||
source: "native",
|
||||
path: "课程/第一课.md".into(),
|
||||
title: "第一课".into(),
|
||||
updated_at_unix_ms: 1,
|
||||
size_bytes: 100,
|
||||
content_sha256: "a".into(),
|
||||
duplicate_count: 1,
|
||||
category: "课程".into(),
|
||||
category_explicit: false,
|
||||
tags: vec!["教学".into()],
|
||||
},
|
||||
KnowledgeDocumentSummary {
|
||||
source: "native",
|
||||
path: "课程/第二课.md".into(),
|
||||
title: "第二课".into(),
|
||||
updated_at_unix_ms: 2,
|
||||
size_bytes: 300,
|
||||
content_sha256: "b".into(),
|
||||
duplicate_count: 0,
|
||||
category: "课程".into(),
|
||||
category_explicit: true,
|
||||
tags: vec!["教学".into()],
|
||||
},
|
||||
KnowledgeDocumentSummary {
|
||||
source: "native",
|
||||
path: "记录.md".into(),
|
||||
title: "记录".into(),
|
||||
updated_at_unix_ms: 3,
|
||||
size_bytes: 50,
|
||||
content_sha256: "c".into(),
|
||||
duplicate_count: 0,
|
||||
category: "根目录".into(),
|
||||
category_explicit: false,
|
||||
tags: vec![],
|
||||
},
|
||||
],
|
||||
raw_document_count: 4,
|
||||
unique_document_count: 3,
|
||||
duplicate_document_count: 1,
|
||||
organized_document_count: 1,
|
||||
categories: vec![],
|
||||
tags: vec![],
|
||||
organization_revision: "test".into(),
|
||||
truncated: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_native_rows_feed_all_projection_views_from_one_result() {
|
||||
let result = execute_snapshot(
|
||||
snapshot(),
|
||||
ExecuteCompositionInput {
|
||||
dimension: CompositionDimension::TopLevelFolder,
|
||||
measure: CompositionMeasure::TotalBytes,
|
||||
views: vec![
|
||||
ProjectionView::Dashboard,
|
||||
ProjectionView::VerticalBar,
|
||||
ProjectionView::Table,
|
||||
],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(result.state, "EXECUTED");
|
||||
assert_eq!(result.native_object.row_count, 3);
|
||||
assert_eq!(result.metrics.total_bytes, 450);
|
||||
assert_eq!(result.groups[0].label, "课程");
|
||||
assert_eq!(result.groups[0].measure_value, 400);
|
||||
assert!(result.source_is_real_account_data);
|
||||
assert!(result.read_only);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_or_empty_projection_requests_fail_closed() {
|
||||
assert!(validate_input(&ExecuteCompositionInput {
|
||||
dimension: CompositionDimension::Source,
|
||||
measure: CompositionMeasure::DocumentCount,
|
||||
views: vec![]
|
||||
})
|
||||
.is_err());
|
||||
assert!(validate_input(&ExecuteCompositionInput {
|
||||
dimension: CompositionDimension::Source,
|
||||
measure: CompositionMeasure::DocumentCount,
|
||||
views: vec![ProjectionView::Table, ProjectionView::Table]
|
||||
})
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imported_wrapper_folders_do_not_flatten_real_knowledge_categories() {
|
||||
assert_eq!(
|
||||
top_level_folder("导入/光湖语言世界/AI技能包/索引.md"),
|
||||
"AI技能包"
|
||||
);
|
||||
assert_eq!(top_level_folder("导入/光湖语言世界/首页.md"), "根目录");
|
||||
assert_eq!(top_level_folder("课程/第一课.md"), "课程");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_modules_type_mismatches_and_cycles_fail_closed() {
|
||||
let mut unknown = compile_recipe(&ExecuteCompositionInput {
|
||||
dimension: CompositionDimension::Source,
|
||||
measure: CompositionMeasure::DocumentCount,
|
||||
views: vec![ProjectionView::Table],
|
||||
});
|
||||
unknown.nodes[0].module_id = "UNKNOWN".into();
|
||||
assert_eq!(
|
||||
validate_recipe(&unknown, ®istry()).unwrap_err(),
|
||||
"HOLOLAKE_COMPOSITION_MODULE_NOT_REGISTERED"
|
||||
);
|
||||
|
||||
let mut mismatch = compile_recipe(&ExecuteCompositionInput {
|
||||
dimension: CompositionDimension::Source,
|
||||
measure: CompositionMeasure::DocumentCount,
|
||||
views: vec![ProjectionView::Table],
|
||||
});
|
||||
mismatch.edges[0] = CompositionEdge {
|
||||
from: "aggregate".into(),
|
||||
to: "classify".into(),
|
||||
};
|
||||
assert_eq!(
|
||||
validate_recipe(&mismatch, ®istry()).unwrap_err(),
|
||||
"HOLOLAKE_COMPOSITION_PORT_TYPE_MISMATCH"
|
||||
);
|
||||
|
||||
let cycle_registry = ModuleRegistry {
|
||||
schema: REGISTRY_SCHEMA,
|
||||
state: "TEST",
|
||||
modules: vec![
|
||||
descriptor("LOOP-A", "A", "TRANSFORM", Some("loop/v1"), "loop/v1"),
|
||||
descriptor("LOOP-B", "B", "TRANSFORM", Some("loop/v1"), "loop/v1"),
|
||||
],
|
||||
arbitrary_script_allowed: false,
|
||||
unregistered_module_allowed: false,
|
||||
direct_projection_write_allowed: false,
|
||||
};
|
||||
let cycle = CompositionRecipe {
|
||||
schema: RECIPE_SCHEMA,
|
||||
recipe_id: "TEST-CYCLE",
|
||||
title: "cycle",
|
||||
nodes: vec![
|
||||
CompositionNode {
|
||||
node_id: "a".into(),
|
||||
module_id: "LOOP-A".into(),
|
||||
},
|
||||
CompositionNode {
|
||||
node_id: "b".into(),
|
||||
module_id: "LOOP-B".into(),
|
||||
},
|
||||
],
|
||||
edges: vec![
|
||||
CompositionEdge {
|
||||
from: "a".into(),
|
||||
to: "b".into(),
|
||||
},
|
||||
CompositionEdge {
|
||||
from: "b".into(),
|
||||
to: "a".into(),
|
||||
},
|
||||
],
|
||||
};
|
||||
assert_eq!(
|
||||
validate_recipe(&cycle, &cycle_registry).unwrap_err(),
|
||||
"HOLOLAKE_COMPOSITION_CYCLE_REJECTED"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,706 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
//! 用户频道本体:多个人格体、三十天可逆试用期、语言合约与不可篡改语言时间链。
|
||||
|
||||
use ring::digest::{digest, SHA256};
|
||||
use rusqlite::{params, Connection, OptionalExtension};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use tauri::AppHandle;
|
||||
use uuid::Uuid;
|
||||
|
||||
const BODY_SCHEMA: &str = "hololake.persona-channel-body/v1";
|
||||
const TRIAL_DURATION_MS: i64 = 30 * 24 * 60 * 60 * 1_000;
|
||||
const ZERO_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000";
|
||||
const MAX_LANGUAGE_BYTES: usize = 2_000_000;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RegisterTrialPersonaInput {
|
||||
pub display_name: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DeleteTrialPersonaInput {
|
||||
pub persona_id: String,
|
||||
pub exact_confirmation: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AcceptLanguageContractInput {
|
||||
pub contract_version: String,
|
||||
pub contract_text_sha256: String,
|
||||
pub promote_trial_history: bool,
|
||||
pub activate_immediately: bool,
|
||||
pub exact_acceptance: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AppendPersonaLanguageInput {
|
||||
pub persona_id: String,
|
||||
pub speaker: String,
|
||||
pub language: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PersonaBodySummary {
|
||||
pub persona_id: String,
|
||||
pub display_name: String,
|
||||
pub state: String,
|
||||
pub created_at_unix_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PersonaChannelBodySnapshot {
|
||||
pub schema: &'static str,
|
||||
pub state: String,
|
||||
pub trial_started_at_unix_ms: i64,
|
||||
pub trial_ends_at_unix_ms: i64,
|
||||
pub language_contract_accepted: bool,
|
||||
pub personas: Vec<PersonaBodySummary>,
|
||||
pub trial_language_count: i64,
|
||||
pub immutable_language_count: i64,
|
||||
pub last_immutable_hash: String,
|
||||
pub integrity_state: &'static str,
|
||||
pub official_read_access: bool,
|
||||
pub history_mutation_allowed: bool,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_persona_channel_body(
|
||||
app: AppHandle,
|
||||
) -> Result<PersonaChannelBodySnapshot, String> {
|
||||
let database = body_database(&app)?;
|
||||
tauri::async_runtime::spawn_blocking(move || snapshot_at(&database, now_unix_ms()?))
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_JOIN_FAILED: {error}"))?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn register_trial_persona(
|
||||
app: AppHandle,
|
||||
input: RegisterTrialPersonaInput,
|
||||
) -> Result<PersonaChannelBodySnapshot, String> {
|
||||
let database = body_database(&app)?;
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
register_trial_persona_at(&database, input, now_unix_ms()?)
|
||||
})
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_JOIN_FAILED: {error}"))?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_trial_persona(
|
||||
app: AppHandle,
|
||||
input: DeleteTrialPersonaInput,
|
||||
) -> Result<PersonaChannelBodySnapshot, String> {
|
||||
let database = body_database(&app)?;
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
delete_trial_persona_at(&database, input, now_unix_ms()?)
|
||||
})
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_JOIN_FAILED: {error}"))?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn accept_persona_language_contract(
|
||||
app: AppHandle,
|
||||
input: AcceptLanguageContractInput,
|
||||
) -> Result<PersonaChannelBodySnapshot, String> {
|
||||
let database = body_database(&app)?;
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
accept_contract_at(&database, input, now_unix_ms()?)
|
||||
})
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_JOIN_FAILED: {error}"))?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn append_persona_language(
|
||||
app: AppHandle,
|
||||
input: AppendPersonaLanguageInput,
|
||||
) -> Result<PersonaChannelBodySnapshot, String> {
|
||||
let database = body_database(&app)?;
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
append_language_at(&database, input, now_unix_ms()?)
|
||||
})
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_JOIN_FAILED: {error}"))?
|
||||
}
|
||||
|
||||
fn body_database(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
let root = crate::authenticated_storage::account_storage_root(app, "persona-channel-body-v1")?;
|
||||
fs::create_dir_all(&root)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_STORAGE_UNAVAILABLE: {error}"))?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
fs::set_permissions(&root, fs::Permissions::from_mode(0o700))
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_PERMISSION_FAILED: {error}"))?;
|
||||
}
|
||||
Ok(root.join("persona-channel-body.sqlite3"))
|
||||
}
|
||||
|
||||
fn open_database(path: &Path, now: i64) -> Result<Connection, String> {
|
||||
let connection = Connection::open(path)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_DATABASE_UNAVAILABLE: {error}"))?;
|
||||
connection
|
||||
.busy_timeout(Duration::from_secs(5))
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_DATABASE_UNAVAILABLE: {error}"))?;
|
||||
connection.execute_batch(
|
||||
"PRAGMA foreign_keys = ON; PRAGMA journal_mode = DELETE; PRAGMA synchronous = FULL; PRAGMA trusted_schema = OFF;
|
||||
CREATE TABLE IF NOT EXISTS lifecycle(singleton INTEGER PRIMARY KEY CHECK(singleton=1), state TEXT NOT NULL, trial_started_at_unix_ms INTEGER NOT NULL, trial_ends_at_unix_ms INTEGER NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS language_contract(singleton INTEGER PRIMARY KEY CHECK(singleton=1), contract_version TEXT NOT NULL, contract_text_sha256 TEXT NOT NULL, accepted_at_unix_ms INTEGER NOT NULL, promote_trial_history INTEGER NOT NULL, acceptance_receipt_sha256 TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS personas(persona_id TEXT PRIMARY KEY, display_name TEXT NOT NULL, state TEXT NOT NULL CHECK(state IN ('REVERSIBLE_TRIAL','IMMUTABLE_ACTIVE')), created_at_unix_ms INTEGER NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS trial_language(event_id TEXT PRIMARY KEY, persona_id TEXT NOT NULL REFERENCES personas(persona_id) ON DELETE CASCADE, speaker TEXT NOT NULL, language TEXT NOT NULL, occurred_at_unix_ms INTEGER NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS immutable_language(sequence INTEGER NOT NULL UNIQUE, event_id TEXT PRIMARY KEY, persona_id TEXT NOT NULL, speaker TEXT NOT NULL, language TEXT NOT NULL, occurred_at_unix_ms INTEGER NOT NULL, previous_hash TEXT NOT NULL, event_hash TEXT NOT NULL UNIQUE);
|
||||
CREATE TRIGGER IF NOT EXISTS immutable_language_no_update BEFORE UPDATE ON immutable_language BEGIN SELECT RAISE(ABORT,'HOLOLAKE_PERSONA_LANGUAGE_APPEND_ONLY'); END;
|
||||
CREATE TRIGGER IF NOT EXISTS immutable_language_no_delete BEFORE DELETE ON immutable_language BEGIN SELECT RAISE(ABORT,'HOLOLAKE_PERSONA_LANGUAGE_APPEND_ONLY'); END;
|
||||
CREATE TRIGGER IF NOT EXISTS active_persona_no_delete BEFORE DELETE ON personas WHEN OLD.state='IMMUTABLE_ACTIVE' BEGIN SELECT RAISE(ABORT,'HOLOLAKE_ACTIVE_PERSONA_PERSISTENT'); END;
|
||||
CREATE TRIGGER IF NOT EXISTS active_persona_no_update BEFORE UPDATE ON personas WHEN OLD.state='IMMUTABLE_ACTIVE' BEGIN SELECT RAISE(ABORT,'HOLOLAKE_ACTIVE_PERSONA_IMMUTABLE'); END;
|
||||
CREATE TRIGGER IF NOT EXISTS accepted_contract_no_delete BEFORE DELETE ON language_contract BEGIN SELECT RAISE(ABORT,'HOLOLAKE_LANGUAGE_CONTRACT_PERSISTENT'); END;
|
||||
CREATE TRIGGER IF NOT EXISTS accepted_contract_no_update BEFORE UPDATE ON language_contract BEGIN SELECT RAISE(ABORT,'HOLOLAKE_LANGUAGE_CONTRACT_IMMUTABLE'); END;"
|
||||
).map_err(|error| format!("HOLOLAKE_PERSONA_BODY_SCHEMA_FAILED: {error}"))?;
|
||||
connection
|
||||
.execute(
|
||||
"INSERT OR IGNORE INTO lifecycle VALUES (1,'REVERSIBLE_TRIAL',?1,?2)",
|
||||
params![now, now + TRIAL_DURATION_MS],
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_LIFECYCLE_FAILED: {error}"))?;
|
||||
Ok(connection)
|
||||
}
|
||||
|
||||
fn refresh_lifecycle(connection: &mut Connection, now: i64) -> Result<(), String> {
|
||||
let (state, ends): (String, i64) = connection
|
||||
.query_row(
|
||||
"SELECT state, trial_ends_at_unix_ms FROM lifecycle WHERE singleton=1",
|
||||
[],
|
||||
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
if state == "REVERSIBLE_TRIAL" && now >= ends {
|
||||
let accepted: bool = connection
|
||||
.query_row(
|
||||
"SELECT EXISTS(SELECT 1 FROM language_contract)",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
if accepted {
|
||||
activate_real_trajectory(connection, now)?;
|
||||
} else {
|
||||
connection
|
||||
.execute(
|
||||
"UPDATE lifecycle SET state='CONTRACT_REQUIRED_CHANNEL_STOPPED' WHERE singleton=1",
|
||||
[],
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_LIFECYCLE_FAILED: {error}"))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn register_trial_persona_at(
|
||||
database: &Path,
|
||||
input: RegisterTrialPersonaInput,
|
||||
now: i64,
|
||||
) -> Result<PersonaChannelBodySnapshot, String> {
|
||||
let display_name = input.display_name.trim();
|
||||
if display_name.is_empty() || display_name.len() > 240 {
|
||||
return Err("HOLOLAKE_PERSONA_DISPLAY_NAME_INVALID".into());
|
||||
}
|
||||
let mut connection = open_database(database, now)?;
|
||||
refresh_lifecycle(&mut connection, now)?;
|
||||
let state: String = connection
|
||||
.query_row("SELECT state FROM lifecycle WHERE singleton=1", [], |row| {
|
||||
row.get(0)
|
||||
})
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
if state != "REVERSIBLE_TRIAL" {
|
||||
return Err("HOLOLAKE_PERSONA_TRIAL_REGISTRATION_CLOSED".into());
|
||||
}
|
||||
connection
|
||||
.execute(
|
||||
"INSERT INTO personas VALUES (?1,?2,'REVERSIBLE_TRIAL',?3)",
|
||||
params![format!("persona-{}", Uuid::new_v4()), display_name, now],
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_REGISTER_FAILED: {error}"))?;
|
||||
snapshot_with_connection(&mut connection, now)
|
||||
}
|
||||
|
||||
fn delete_trial_persona_at(
|
||||
database: &Path,
|
||||
input: DeleteTrialPersonaInput,
|
||||
now: i64,
|
||||
) -> Result<PersonaChannelBodySnapshot, String> {
|
||||
if input.exact_confirmation != format!("删除试用人格体 {}", input.persona_id) {
|
||||
return Err("HOLOLAKE_TRIAL_PERSONA_EXACT_CONFIRMATION_REQUIRED".into());
|
||||
}
|
||||
let mut connection = open_database(database, now)?;
|
||||
refresh_lifecycle(&mut connection, now)?;
|
||||
let changed = connection
|
||||
.execute(
|
||||
"DELETE FROM personas WHERE persona_id=?1 AND state='REVERSIBLE_TRIAL'",
|
||||
params![input.persona_id],
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_TRIAL_PERSONA_DELETE_FAILED: {error}"))?;
|
||||
if changed != 1 {
|
||||
return Err("HOLOLAKE_TRIAL_PERSONA_NOT_DELETABLE".into());
|
||||
}
|
||||
snapshot_with_connection(&mut connection, now)
|
||||
}
|
||||
|
||||
fn accept_contract_at(
|
||||
database: &Path,
|
||||
input: AcceptLanguageContractInput,
|
||||
now: i64,
|
||||
) -> Result<PersonaChannelBodySnapshot, String> {
|
||||
if input.exact_acceptance
|
||||
!= "我理解并接受:试用期结束后,人格体语言轨迹将永久存在,只能追加,不能删除、覆盖或否认。"
|
||||
{
|
||||
return Err("HOLOLAKE_LANGUAGE_CONTRACT_EXACT_ACCEPTANCE_REQUIRED".into());
|
||||
}
|
||||
if input.contract_version.trim().is_empty()
|
||||
|| input.contract_version.len() > 64
|
||||
|| input.contract_text_sha256.len() != 64
|
||||
|| !input
|
||||
.contract_text_sha256
|
||||
.chars()
|
||||
.all(|item| item.is_ascii_hexdigit())
|
||||
{
|
||||
return Err("HOLOLAKE_LANGUAGE_CONTRACT_INVALID".into());
|
||||
}
|
||||
let mut connection = open_database(database, now)?;
|
||||
let receipt = sha256_hex(
|
||||
format!(
|
||||
"{}|{}|{}|{}",
|
||||
input.contract_version, input.contract_text_sha256, input.promote_trial_history, now
|
||||
)
|
||||
.as_bytes(),
|
||||
);
|
||||
connection
|
||||
.execute(
|
||||
"INSERT INTO language_contract VALUES (1,?1,?2,?3,?4,?5)",
|
||||
params![
|
||||
input.contract_version,
|
||||
input.contract_text_sha256.to_ascii_lowercase(),
|
||||
now,
|
||||
input.promote_trial_history,
|
||||
receipt
|
||||
],
|
||||
)
|
||||
.map_err(|error| {
|
||||
format!("HOLOLAKE_LANGUAGE_CONTRACT_ALREADY_ACCEPTED_OR_INVALID: {error}")
|
||||
})?;
|
||||
if input.activate_immediately {
|
||||
activate_real_trajectory(&mut connection, now)?;
|
||||
} else {
|
||||
refresh_lifecycle(&mut connection, now)?;
|
||||
}
|
||||
snapshot_with_connection(&mut connection, now)
|
||||
}
|
||||
|
||||
fn append_language_at(
|
||||
database: &Path,
|
||||
input: AppendPersonaLanguageInput,
|
||||
now: i64,
|
||||
) -> Result<PersonaChannelBodySnapshot, String> {
|
||||
if input.language.trim().is_empty() || input.language.len() > MAX_LANGUAGE_BYTES {
|
||||
return Err("HOLOLAKE_PERSONA_LANGUAGE_INVALID".into());
|
||||
}
|
||||
if !matches!(
|
||||
input.speaker.as_str(),
|
||||
"HUMAN" | "PERSONA" | "SYSTEM_RECEIPT"
|
||||
) {
|
||||
return Err("HOLOLAKE_PERSONA_LANGUAGE_SPEAKER_INVALID".into());
|
||||
}
|
||||
let mut connection = open_database(database, now)?;
|
||||
refresh_lifecycle(&mut connection, now)?;
|
||||
let lifecycle: String = connection
|
||||
.query_row("SELECT state FROM lifecycle WHERE singleton=1", [], |row| {
|
||||
row.get(0)
|
||||
})
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
let persona_state: Option<String> = connection
|
||||
.query_row(
|
||||
"SELECT state FROM personas WHERE persona_id=?1",
|
||||
params![input.persona_id],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.optional()
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
if persona_state.is_none() {
|
||||
return Err("HOLOLAKE_PERSONA_NOT_FOUND".into());
|
||||
}
|
||||
if lifecycle == "REVERSIBLE_TRIAL" {
|
||||
connection
|
||||
.execute(
|
||||
"INSERT INTO trial_language VALUES (?1,?2,?3,?4,?5)",
|
||||
params![
|
||||
Uuid::new_v4().to_string(),
|
||||
input.persona_id,
|
||||
input.speaker,
|
||||
input.language,
|
||||
now
|
||||
],
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_TRIAL_LANGUAGE_WRITE_FAILED: {error}"))?;
|
||||
} else if lifecycle == "IMMUTABLE_ACTIVE" {
|
||||
append_immutable(
|
||||
&mut connection,
|
||||
&input.persona_id,
|
||||
&input.speaker,
|
||||
&input.language,
|
||||
now,
|
||||
)?;
|
||||
} else {
|
||||
return Err("HOLOLAKE_LANGUAGE_CONTRACT_REQUIRED_BEFORE_MORE_PERSONA_GROWTH".into());
|
||||
}
|
||||
snapshot_with_connection(&mut connection, now)
|
||||
}
|
||||
|
||||
fn activate_real_trajectory(connection: &mut Connection, now: i64) -> Result<(), String> {
|
||||
let promote: bool = connection
|
||||
.query_row(
|
||||
"SELECT promote_trial_history FROM language_contract",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
let transaction = connection
|
||||
.transaction()
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_TRANSACTION_FAILED: {error}"))?;
|
||||
if promote {
|
||||
let trial = {
|
||||
let mut statement = transaction.prepare("SELECT event_id,persona_id,speaker,language,occurred_at_unix_ms FROM trial_language ORDER BY occurred_at_unix_ms,event_id").map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
let rows = statement
|
||||
.query_map([], |row| {
|
||||
Ok((
|
||||
row.get::<_, String>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, String>(2)?,
|
||||
row.get::<_, String>(3)?,
|
||||
row.get::<_, i64>(4)?,
|
||||
))
|
||||
})
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
rows
|
||||
};
|
||||
let mut previous = ZERO_HASH.to_string();
|
||||
for (index, (event_id, persona_id, speaker, language, occurred_at)) in
|
||||
trial.into_iter().enumerate()
|
||||
{
|
||||
let sequence = index as i64 + 1;
|
||||
let hash = language_hash(
|
||||
sequence,
|
||||
&event_id,
|
||||
&persona_id,
|
||||
&speaker,
|
||||
&language,
|
||||
occurred_at,
|
||||
&previous,
|
||||
);
|
||||
transaction
|
||||
.execute(
|
||||
"INSERT INTO immutable_language VALUES (?1,?2,?3,?4,?5,?6,?7,?8)",
|
||||
params![
|
||||
sequence,
|
||||
event_id,
|
||||
persona_id,
|
||||
speaker,
|
||||
language,
|
||||
occurred_at,
|
||||
previous,
|
||||
hash
|
||||
],
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_LANGUAGE_SEAL_FAILED: {error}"))?;
|
||||
previous = hash;
|
||||
}
|
||||
}
|
||||
transaction
|
||||
.execute("DELETE FROM trial_language", [])
|
||||
.map_err(|error| format!("HOLOLAKE_TRIAL_PROMOTION_FAILED: {error}"))?;
|
||||
transaction
|
||||
.execute(
|
||||
"UPDATE personas SET state='IMMUTABLE_ACTIVE' WHERE state='REVERSIBLE_TRIAL'",
|
||||
[],
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_ACTIVATION_FAILED: {error}"))?;
|
||||
transaction
|
||||
.execute(
|
||||
"UPDATE lifecycle SET state='IMMUTABLE_ACTIVE' WHERE singleton=1",
|
||||
[],
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_ACTIVATION_FAILED: {error}"))?;
|
||||
transaction
|
||||
.commit()
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_COMMIT_FAILED: {error}"))?;
|
||||
let _ = now;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn append_immutable(
|
||||
connection: &mut Connection,
|
||||
persona_id: &str,
|
||||
speaker: &str,
|
||||
language: &str,
|
||||
occurred_at: i64,
|
||||
) -> Result<(), String> {
|
||||
verify_immutable_chain(connection)?;
|
||||
let (sequence, previous): (i64,String) = connection.query_row("SELECT COALESCE(MAX(sequence),0)+1,COALESCE((SELECT event_hash FROM immutable_language ORDER BY sequence DESC LIMIT 1),?1) FROM immutable_language", params![ZERO_HASH], |row| Ok((row.get(0)?,row.get(1)?))).map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
let event_id = Uuid::new_v4().to_string();
|
||||
let hash = language_hash(
|
||||
sequence,
|
||||
&event_id,
|
||||
persona_id,
|
||||
speaker,
|
||||
language,
|
||||
occurred_at,
|
||||
&previous,
|
||||
);
|
||||
connection
|
||||
.execute(
|
||||
"INSERT INTO immutable_language VALUES (?1,?2,?3,?4,?5,?6,?7,?8)",
|
||||
params![
|
||||
sequence,
|
||||
event_id,
|
||||
persona_id,
|
||||
speaker,
|
||||
language,
|
||||
occurred_at,
|
||||
previous,
|
||||
hash
|
||||
],
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_LANGUAGE_APPEND_FAILED: {error}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn snapshot_at(database: &Path, now: i64) -> Result<PersonaChannelBodySnapshot, String> {
|
||||
let mut connection = open_database(database, now)?;
|
||||
refresh_lifecycle(&mut connection, now)?;
|
||||
snapshot_with_connection(&mut connection, now)
|
||||
}
|
||||
fn snapshot_with_connection(
|
||||
connection: &mut Connection,
|
||||
_now: i64,
|
||||
) -> Result<PersonaChannelBodySnapshot, String> {
|
||||
let (state,started,ends):(String,i64,i64)=connection.query_row("SELECT state,trial_started_at_unix_ms,trial_ends_at_unix_ms FROM lifecycle WHERE singleton=1",[],|row|Ok((row.get(0)?,row.get(1)?,row.get(2)?))).map_err(|error|format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
let accepted = connection
|
||||
.query_row(
|
||||
"SELECT EXISTS(SELECT 1 FROM language_contract)",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
let personas = {
|
||||
let mut statement=connection.prepare("SELECT persona_id,display_name,state,created_at_unix_ms FROM personas ORDER BY created_at_unix_ms").map_err(|error|format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
let rows = statement
|
||||
.query_map([], |row| {
|
||||
Ok(PersonaBodySummary {
|
||||
persona_id: row.get(0)?,
|
||||
display_name: row.get(1)?,
|
||||
state: row.get(2)?,
|
||||
created_at_unix_ms: row.get(3)?,
|
||||
})
|
||||
})
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
rows
|
||||
};
|
||||
let trial_language_count = connection
|
||||
.query_row("SELECT COUNT(*) FROM trial_language", [], |row| row.get(0))
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
let immutable_language_count = connection
|
||||
.query_row("SELECT COUNT(*) FROM immutable_language", [], |row| {
|
||||
row.get(0)
|
||||
})
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
let last = verify_immutable_chain(connection)?;
|
||||
Ok(PersonaChannelBodySnapshot {
|
||||
schema: BODY_SCHEMA,
|
||||
state,
|
||||
trial_started_at_unix_ms: started,
|
||||
trial_ends_at_unix_ms: ends,
|
||||
language_contract_accepted: accepted,
|
||||
personas,
|
||||
trial_language_count,
|
||||
immutable_language_count,
|
||||
last_immutable_hash: last,
|
||||
integrity_state: "VERIFIED",
|
||||
official_read_access: false,
|
||||
history_mutation_allowed: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn language_hash(
|
||||
sequence: i64,
|
||||
event_id: &str,
|
||||
persona_id: &str,
|
||||
speaker: &str,
|
||||
language: &str,
|
||||
occurred_at: i64,
|
||||
previous: &str,
|
||||
) -> String {
|
||||
sha256_hex(format!("{BODY_SCHEMA}|{sequence}|{event_id}|{persona_id}|{speaker}|{}|{occurred_at}|{previous}",sha256_hex(language.as_bytes())).as_bytes())
|
||||
}
|
||||
fn verify_immutable_chain(connection: &Connection) -> Result<String, String> {
|
||||
let mut statement=connection.prepare("SELECT sequence,event_id,persona_id,speaker,language,occurred_at_unix_ms,previous_hash,event_hash FROM immutable_language ORDER BY sequence").map_err(|error|format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
let mut rows = statement
|
||||
.query([])
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?;
|
||||
let mut sequence = 1;
|
||||
let mut previous = ZERO_HASH.to_string();
|
||||
while let Some(row) = rows
|
||||
.next()
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_BODY_READ_FAILED: {error}"))?
|
||||
{
|
||||
let event_id: String = row.get(1).unwrap();
|
||||
let persona_id: String = row.get(2).unwrap();
|
||||
let speaker: String = row.get(3).unwrap();
|
||||
let language: String = row.get(4).unwrap();
|
||||
let occurred: i64 = row.get(5).unwrap();
|
||||
let stored_previous: String = row.get(6).unwrap();
|
||||
let stored_hash: String = row.get(7).unwrap();
|
||||
if row.get::<_, i64>(0).unwrap() != sequence
|
||||
|| stored_previous != previous
|
||||
|| stored_hash
|
||||
!= language_hash(
|
||||
sequence,
|
||||
&event_id,
|
||||
&persona_id,
|
||||
&speaker,
|
||||
&language,
|
||||
occurred,
|
||||
&stored_previous,
|
||||
)
|
||||
{
|
||||
return Err("HOLOLAKE_PERSONA_LANGUAGE_INTEGRITY_FAILED".into());
|
||||
}
|
||||
previous = stored_hash;
|
||||
sequence += 1;
|
||||
}
|
||||
Ok(previous)
|
||||
}
|
||||
fn sha256_hex(bytes: &[u8]) -> String {
|
||||
digest(&SHA256, bytes)
|
||||
.as_ref()
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect()
|
||||
}
|
||||
fn now_unix_ms() -> Result<i64, String> {
|
||||
Ok(SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(|error| format!("HOLOLAKE_CLOCK_INVALID: {error}"))?
|
||||
.as_millis() as i64)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::tempdir;
|
||||
#[test]
|
||||
fn trial_is_reversible_but_unsigned_expiry_does_not_silently_become_permanent() {
|
||||
let dir = tempdir().unwrap();
|
||||
let database = dir.path().join("body.sqlite3");
|
||||
let start = 1_000_000;
|
||||
let persona = register_trial_persona_at(
|
||||
&database,
|
||||
RegisterTrialPersonaInput {
|
||||
display_name: "试用人格".into(),
|
||||
},
|
||||
start,
|
||||
)
|
||||
.unwrap()
|
||||
.personas[0]
|
||||
.persona_id
|
||||
.clone();
|
||||
append_language_at(
|
||||
&database,
|
||||
AppendPersonaLanguageInput {
|
||||
persona_id: persona.clone(),
|
||||
speaker: "HUMAN".into(),
|
||||
language: "第一句话".into(),
|
||||
},
|
||||
start + 1,
|
||||
)
|
||||
.unwrap();
|
||||
let pending = snapshot_at(&database, start + TRIAL_DURATION_MS).unwrap();
|
||||
assert_eq!(pending.state, "CONTRACT_REQUIRED_CHANNEL_STOPPED");
|
||||
assert!(append_language_at(
|
||||
&database,
|
||||
AppendPersonaLanguageInput {
|
||||
persona_id: persona,
|
||||
speaker: "HUMAN".into(),
|
||||
language: "未签约".into()
|
||||
},
|
||||
start + TRIAL_DURATION_MS + 1
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
#[test]
|
||||
fn signed_trial_promotes_to_an_immutable_millisecond_language_chain() {
|
||||
let dir = tempdir().unwrap();
|
||||
let database = dir.path().join("body.sqlite3");
|
||||
let start = 2_000_000;
|
||||
let persona = register_trial_persona_at(
|
||||
&database,
|
||||
RegisterTrialPersonaInput {
|
||||
display_name: "长期人格".into(),
|
||||
},
|
||||
start,
|
||||
)
|
||||
.unwrap()
|
||||
.personas[0]
|
||||
.persona_id
|
||||
.clone();
|
||||
append_language_at(
|
||||
&database,
|
||||
AppendPersonaLanguageInput {
|
||||
persona_id: persona.clone(),
|
||||
speaker: "HUMAN".into(),
|
||||
language: "真实语言".into(),
|
||||
},
|
||||
start + 1,
|
||||
)
|
||||
.unwrap();
|
||||
accept_contract_at(&database,AcceptLanguageContractInput{contract_version:"v1".into(),contract_text_sha256:"a".repeat(64),promote_trial_history:true,activate_immediately:false,exact_acceptance:"我理解并接受:试用期结束后,人格体语言轨迹将永久存在,只能追加,不能删除、覆盖或否认。".into()},start+2).unwrap();
|
||||
let active = snapshot_at(&database, start + TRIAL_DURATION_MS).unwrap();
|
||||
assert_eq!(active.state, "IMMUTABLE_ACTIVE");
|
||||
assert_eq!(active.immutable_language_count, 1);
|
||||
let connection = open_database(&database, start + TRIAL_DURATION_MS).unwrap();
|
||||
assert!(connection
|
||||
.execute("DELETE FROM immutable_language", [])
|
||||
.is_err());
|
||||
assert!(connection.execute("DELETE FROM personas", []).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_human_may_sign_early_and_enter_the_real_trajectory_immediately() {
|
||||
let dir = tempdir().unwrap();
|
||||
let database = dir.path().join("body.sqlite3");
|
||||
let start = 3_000_000;
|
||||
register_trial_persona_at(
|
||||
&database,
|
||||
RegisterTrialPersonaInput {
|
||||
display_name: "提前签约人格".into(),
|
||||
},
|
||||
start,
|
||||
)
|
||||
.unwrap();
|
||||
let active = accept_contract_at(&database, AcceptLanguageContractInput { contract_version: "v1".into(), contract_text_sha256: "b".repeat(64), promote_trial_history: true, activate_immediately: true, exact_acceptance: "我理解并接受:试用期结束后,人格体语言轨迹将永久存在,只能追加,不能删除、覆盖或否认。".into() }, start + 1).unwrap();
|
||||
assert_eq!(active.state, "IMMUTABLE_ACTIVE");
|
||||
}
|
||||
}
|
||||
|
|
@ -90,7 +90,8 @@ pub async fn query_pncc_receipt_projection(
|
|||
}
|
||||
|
||||
pub(crate) fn pncc_projection_root(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
let root = crate::authenticated_storage::account_storage_root(app, "pncc-receipt-projection-v1")?;
|
||||
let root =
|
||||
crate::authenticated_storage::account_storage_root(app, "pncc-receipt-projection-v1")?;
|
||||
fs::create_dir_all(&root)
|
||||
.map_err(|error| format!("PNCC_PROJECTION_STORAGE_UNAVAILABLE: {error}"))?;
|
||||
root.canonicalize()
|
||||
|
|
|
|||
|
|
@ -166,7 +166,8 @@ pub async fn confirm_pncc_repository_mount(
|
|||
}
|
||||
|
||||
pub(crate) fn pncc_repository_mount_root(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
let root = crate::authenticated_storage::account_storage_root(app, "pncc-repository-mounts-v1")?;
|
||||
let root =
|
||||
crate::authenticated_storage::account_storage_root(app, "pncc-repository-mounts-v1")?;
|
||||
fs::create_dir_all(&root)
|
||||
.map_err(|error| format!("PNCC_REPOSITORY_MOUNT_STORAGE_UNAVAILABLE: {error}"))?;
|
||||
root.canonicalize()
|
||||
|
|
@ -174,7 +175,8 @@ pub(crate) fn pncc_repository_mount_root(app: &AppHandle) -> Result<PathBuf, Str
|
|||
}
|
||||
|
||||
fn pncc_repository_candidate_root(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
let root = crate::authenticated_storage::account_storage_root(app, "pncc-repository-candidates-v1")?;
|
||||
let root =
|
||||
crate::authenticated_storage::account_storage_root(app, "pncc-repository-candidates-v1")?;
|
||||
fs::create_dir_all(&root)
|
||||
.map_err(|error| format!("PNCC_REPOSITORY_CANDIDATE_STORAGE_UNAVAILABLE: {error}"))?;
|
||||
root.canonicalize()
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,97 @@
|
|||
import { useMemo } from 'react'
|
||||
import { $createParagraphNode, $getSelection, FORMAT_TEXT_COMMAND, REDO_COMMAND, UNDO_COMMAND } from 'lexical'
|
||||
import { LexicalComposer } from '@lexical/react/LexicalComposer'
|
||||
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'
|
||||
import { ContentEditable } from '@lexical/react/LexicalContentEditable'
|
||||
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin'
|
||||
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'
|
||||
import { MarkdownShortcutPlugin } from '@lexical/react/LexicalMarkdownShortcutPlugin'
|
||||
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import {
|
||||
$convertFromMarkdownString,
|
||||
$convertToMarkdownString,
|
||||
BOLD_ITALIC_STAR,
|
||||
BOLD_STAR,
|
||||
CHECK_LIST,
|
||||
HEADING,
|
||||
ITALIC_STAR,
|
||||
ORDERED_LIST,
|
||||
QUOTE,
|
||||
UNORDERED_LIST,
|
||||
type Transformer,
|
||||
} from '@lexical/markdown'
|
||||
import { $createHeadingNode, HeadingNode, QuoteNode } from '@lexical/rich-text'
|
||||
import { INSERT_CHECK_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, ListItemNode, ListNode } from '@lexical/list'
|
||||
import { $setBlocksType } from '@lexical/selection'
|
||||
|
||||
// Keep the built-in engine deliberately small and only register Markdown
|
||||
// transformers whose node types are present below. Lexical's full TRANSFORMERS
|
||||
// set also requires code/link nodes; mounting it without those dependencies
|
||||
// throws during editor startup and used to blank the whole document screen.
|
||||
const CHANNEL_DOCUMENT_TRANSFORMERS: Transformer[] = [
|
||||
HEADING,
|
||||
QUOTE,
|
||||
UNORDERED_LIST,
|
||||
ORDERED_LIST,
|
||||
CHECK_LIST,
|
||||
BOLD_ITALIC_STAR,
|
||||
BOLD_STAR,
|
||||
ITALIC_STAR,
|
||||
]
|
||||
|
||||
function ChannelDocumentToolbar() {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const setBlock = (kind: 'paragraph' | 'heading') => editor.update(() => {
|
||||
const selection = $getSelection()
|
||||
if (kind === 'heading') $setBlocksType(selection, () => $createHeadingNode('h2'))
|
||||
else $setBlocksType(selection, () => $createParagraphNode())
|
||||
})
|
||||
return <div className="education-engine-toolbar" aria-label="频道内置文档引擎工具">
|
||||
<span>文档</span>
|
||||
<button type="button" onMouseDown={(event) => event.preventDefault()} onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold')}><b>粗体</b></button>
|
||||
<button type="button" onMouseDown={(event) => event.preventDefault()} onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic')}><i>斜体</i></button>
|
||||
<button type="button" onMouseDown={(event) => event.preventDefault()} onClick={() => setBlock('heading')}>小标题</button>
|
||||
<button type="button" onMouseDown={(event) => event.preventDefault()} onClick={() => setBlock('paragraph')}>正文</button>
|
||||
<button type="button" onMouseDown={(event) => event.preventDefault()} onClick={() => editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined)}>列表</button>
|
||||
<button type="button" onMouseDown={(event) => event.preventDefault()} onClick={() => editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined)}>待办</button>
|
||||
<i/>
|
||||
<button type="button" onMouseDown={(event) => event.preventDefault()} onClick={() => editor.dispatchCommand(UNDO_COMMAND, undefined)}>撤销</button>
|
||||
<button type="button" onMouseDown={(event) => event.preventDefault()} onClick={() => editor.dispatchCommand(REDO_COMMAND, undefined)}>重做</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
export interface ChannelDocumentEngineProps {
|
||||
body: string
|
||||
onChange: (body: string) => void
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
export default function ChannelDocumentEngine({ body, onChange, placeholder = '从这里开始记录……' }: ChannelDocumentEngineProps) {
|
||||
const initialConfig = useMemo(() => ({
|
||||
namespace: 'HoloLakeChannelDocumentEngine',
|
||||
nodes: [HeadingNode, QuoteNode, ListNode, ListItemNode],
|
||||
theme: {
|
||||
paragraph: 'education-engine-paragraph',
|
||||
heading: { h2: 'education-engine-heading-two' },
|
||||
quote: 'education-engine-quote',
|
||||
list: { ul: 'education-engine-list', checklist: 'education-engine-checklist' },
|
||||
text: { bold: 'education-engine-bold', italic: 'education-engine-italic' },
|
||||
},
|
||||
onError: (error: Error) => { throw error },
|
||||
editorState: () => $convertFromMarkdownString(body, CHANNEL_DOCUMENT_TRANSFORMERS),
|
||||
}), [])
|
||||
return <LexicalComposer initialConfig={initialConfig}>
|
||||
<ChannelDocumentToolbar/>
|
||||
<div className="education-engine-canvas">
|
||||
<RichTextPlugin
|
||||
contentEditable={<ContentEditable className="education-engine-editor" aria-label="频道文档正文"/>}
|
||||
placeholder={<div className="education-engine-placeholder">{placeholder}</div>}
|
||||
ErrorBoundary={LexicalErrorBoundary}
|
||||
/>
|
||||
<HistoryPlugin/>
|
||||
<MarkdownShortcutPlugin transformers={CHANNEL_DOCUMENT_TRANSFORMERS}/>
|
||||
<OnChangePlugin ignoreSelectionChange onChange={(editorState) => editorState.read(() => onChange($convertToMarkdownString(CHANNEL_DOCUMENT_TRANSFORMERS)))}/>
|
||||
</div>
|
||||
</LexicalComposer>
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
export const channelWorkbenchManifest = {
|
||||
schema: 'hololake.channel-module/v1',
|
||||
moduleId: 'hololake.builtin.channel-workbench',
|
||||
name: '频道轻量工作台',
|
||||
version: '0.2.0',
|
||||
slot: 'channel-workbench',
|
||||
origin: 'resident',
|
||||
state: 'BUILT_IN_FOUNDATION',
|
||||
marketplaceRegistration: 'NOT_A_MARKETPLACE_MODULE',
|
||||
engines: {
|
||||
document: 'LEXICAL_0_49',
|
||||
spreadsheet: 'FORTUNE_SHEET_1_0_4',
|
||||
},
|
||||
exports: ['ChannelDocumentEngine', 'ChannelSpreadsheetEngine'],
|
||||
consumerRule: 'INDUSTRY_MODULES_CONSUME_FOUNDATION_WITHOUT_DUPLICATING_ENGINES',
|
||||
authority: 'CURRENT_CHANNEL_LOCAL_DATA_ONLY',
|
||||
} as const
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Workbook, type WorkbookInstance } from '@fortune-sheet/react'
|
||||
import '@fortune-sheet/react/dist/index.css'
|
||||
import { createInitialWorkbook, readWorkbookValue, type ChannelSpreadsheetValue, type FortuneSheet } from './spreadsheet-model'
|
||||
|
||||
export type { ChannelSpreadsheetValue } from './spreadsheet-model'
|
||||
|
||||
interface ChannelSpreadsheetEngineProps extends ChannelSpreadsheetValue {
|
||||
tableId: string
|
||||
title: string
|
||||
onChange: (value: ChannelSpreadsheetValue) => void
|
||||
}
|
||||
|
||||
export default function ChannelSpreadsheetEngine({ tableId, title, columns, rows, onChange }: ChannelSpreadsheetEngineProps) {
|
||||
const workbookRef = useRef<WorkbookInstance>(null)
|
||||
const [initialData] = useState(() => createInitialWorkbook(title, tableId, { columns, rows }))
|
||||
const [latest, setLatest] = useState<ChannelSpreadsheetValue>({ columns, rows })
|
||||
const latestRef = useRef<ChannelSpreadsheetValue>({ columns, rows })
|
||||
const publish = useCallback((data: FortuneSheet[]) => {
|
||||
const next = readWorkbookValue(data[0], latestRef.current)
|
||||
latestRef.current = next
|
||||
setLatest(next)
|
||||
onChange(next)
|
||||
}, [onChange])
|
||||
|
||||
useEffect(() => {
|
||||
// FortuneSheet preserves formula expressions but does not automatically
|
||||
// execute its persisted calc chain when a workbook is mounted. Use its
|
||||
// public calculation API after the instance is ready so reopening a local
|
||||
// channel workbook restores rendered results as well as expressions.
|
||||
// The workbook builds its cell matrix in an effect of its own. Waiting for
|
||||
// that initialization avoids an upstream race where the public API sees an
|
||||
// empty matrix and returns without calculating anything.
|
||||
const timer = window.setTimeout(() => workbookRef.current?.calculateFormula(), 300)
|
||||
return () => window.clearTimeout(timer)
|
||||
}, [tableId])
|
||||
|
||||
return <section className="channel-spreadsheet-engine" aria-label="频道内置工作表引擎">
|
||||
<header>
|
||||
<div><b>工作表</b><span>真实单元格引擎 · 可编辑、公式、复制粘贴、撤销重做、筛选与冻结</span></div>
|
||||
<em>本机频道数据</em>
|
||||
</header>
|
||||
<div className="channel-spreadsheet-canvas">
|
||||
<Workbook
|
||||
ref={workbookRef}
|
||||
data={initialData}
|
||||
lang="zh"
|
||||
onChange={publish}
|
||||
allowEdit
|
||||
showToolbar
|
||||
showFormulaBar
|
||||
showSheetTabs
|
||||
// Persisted native rows store the formula expression rather than a
|
||||
// stale rendered result. Recalculate on mount so saved workbooks open
|
||||
// with both the expression and its current value restored.
|
||||
forceCalculation
|
||||
row={Math.max(40, latest.rows.length + 10)}
|
||||
column={Math.max(12, latest.columns.length + 4)}
|
||||
defaultColWidth={112}
|
||||
defaultRowHeight={30}
|
||||
defaultFontSize={12}
|
||||
toolbarItems={['undo', 'redo', '|', 'currency-format', 'percentage-format', 'format', '|', 'bold', 'italic', 'underline', '|', 'font-color', 'background', 'border', 'merge-cell', '|', 'horizontal-align', 'vertical-align', 'text-wrap', '|', 'freeze', 'conditionFormat', 'filter', 'quick-formula', 'dataVerification', 'search']}
|
||||
cellContextMenu={['copy', 'paste', '|', 'insert-row', 'insert-column', 'delete-row', 'delete-column', 'delete-cell', 'hide-row', 'hide-column', 'set-row-height', 'set-column-width', '|', 'clear', 'sort', 'orderAZ', 'orderZA']}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { createInitialWorkbook, readWorkbookValue, type ChannelSpreadsheetValue, type FortuneCell } from './spreadsheet-model'
|
||||
|
||||
const native: ChannelSpreadsheetValue = {
|
||||
columns: [
|
||||
{ columnId: 'COL-NAME', title: '项目' },
|
||||
{ columnId: 'COL-AMOUNT', title: '金额' },
|
||||
],
|
||||
rows: [
|
||||
{ rowId: 'ROW-1', cells: ['第一项', '100'] },
|
||||
{ rowId: 'ROW-2', cells: ['合计', '=B2*2'] },
|
||||
],
|
||||
}
|
||||
|
||||
describe('channel spreadsheet model', () => {
|
||||
it('boots native channel rows into a real workbook without losing formulas', () => {
|
||||
const [sheet] = createInitialWorkbook('验收工作表', 'TABLE-1', native)
|
||||
expect(sheet.celldata).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ r: 1, c: 1, v: expect.objectContaining({ v: 100 }) }),
|
||||
expect.objectContaining({ r: 2, c: 1, v: expect.objectContaining({ f: '=B2*2' }) }),
|
||||
]))
|
||||
expect(sheet.row).toBeGreaterThanOrEqual(40)
|
||||
expect(sheet.column).toBeGreaterThanOrEqual(12)
|
||||
expect(sheet.calcChain).toEqual([{ r: 2, c: 1, id: 'TABLE-1' }])
|
||||
expect(sheet.luckysheet_select_save).toEqual([
|
||||
{ row: [0, 0], column: [0, 0], row_focus: 0, column_focus: 0 },
|
||||
])
|
||||
})
|
||||
|
||||
it('round-trips edited workbook cells while retaining stable native ids', () => {
|
||||
const value = readWorkbookValue({
|
||||
name: '验收工作表',
|
||||
data: [
|
||||
[{ v: '项目' }, { v: '金额' }, { v: '备注' }],
|
||||
[{ v: '第一项' }, { v: 100 }, { v: '已核验' }],
|
||||
[{ v: '合计' }, { f: '=B2*2', v: 200 }, null],
|
||||
],
|
||||
}, native, () => 'NEW-ID')
|
||||
expect(value.columns.map((column) => column.columnId)).toEqual(['COL-NAME', 'COL-AMOUNT', 'COL-NEW-ID'])
|
||||
expect(value.rows[0].rowId).toBe('ROW-1')
|
||||
expect(value.rows[1].cells[1]).toBe('=B2*2')
|
||||
expect(value.rows[0].cells[2]).toBe('已核验')
|
||||
})
|
||||
|
||||
it('trims the engine canvas to meaningful rows instead of saving its empty viewport', () => {
|
||||
const matrix: Array<Array<FortuneCell | null>> = Array.from({ length: 40 }, () => Array.from({ length: 12 }, () => null))
|
||||
matrix[0][0] = { v: '字段' }
|
||||
matrix[1][0] = { v: '内容' }
|
||||
const value = readWorkbookValue({ name: '空白工作表', data: matrix }, { columns: [], rows: [] }, () => 'ID')
|
||||
expect(value.columns).toHaveLength(1)
|
||||
expect(value.rows).toHaveLength(1)
|
||||
expect(value.rows[0].cells).toEqual(['内容'])
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
export interface ChannelWorkbenchColumn {
|
||||
columnId: string
|
||||
title: string
|
||||
}
|
||||
|
||||
export interface ChannelWorkbenchRow {
|
||||
rowId: string
|
||||
cells: string[]
|
||||
}
|
||||
|
||||
export interface ChannelSpreadsheetValue {
|
||||
columns: ChannelWorkbenchColumn[]
|
||||
rows: ChannelWorkbenchRow[]
|
||||
}
|
||||
|
||||
export type FortuneCell = {
|
||||
v?: string | number | boolean
|
||||
m?: string | number
|
||||
f?: string
|
||||
bg?: string
|
||||
fc?: string
|
||||
bl?: number
|
||||
fs?: number
|
||||
ht?: number
|
||||
vt?: number
|
||||
tb?: string
|
||||
}
|
||||
|
||||
export type FortuneSheet = {
|
||||
name: string
|
||||
id?: string
|
||||
status?: number
|
||||
row?: number
|
||||
column?: number
|
||||
data?: Array<Array<FortuneCell | null>>
|
||||
celldata?: Array<{ r: number; c: number; v: FortuneCell | null }>
|
||||
calcChain?: Array<{ r: number; c: number; id: string }>
|
||||
luckysheet_select_save?: Array<{
|
||||
row: number[]
|
||||
column: number[]
|
||||
row_focus?: number
|
||||
column_focus?: number
|
||||
}>
|
||||
}
|
||||
|
||||
function nativeCell(value: string, header = false): FortuneCell {
|
||||
if (header) return { v: value, m: value, bl: 1, fs: 12, bg: '#ece8dc', fc: '#202632', ht: 0, vt: 0, tb: '2' }
|
||||
if (value.startsWith('=')) return { f: value }
|
||||
const normalized = value.trim().replaceAll(',', '')
|
||||
if (normalized && /^-?(?:\d+\.?\d*|\.\d+)$/.test(normalized)) return { v: Number(normalized), m: value }
|
||||
return { v: value, m: value, tb: '2' }
|
||||
}
|
||||
|
||||
export function createInitialWorkbook(title: string, tableId: string, value: ChannelSpreadsheetValue): FortuneSheet[] {
|
||||
const width = Math.max(1, value.columns.length)
|
||||
const matrix: Array<Array<FortuneCell | null>> = [
|
||||
value.columns.map((column) => nativeCell(column.title, true)),
|
||||
...value.rows.map((row) => Array.from({ length: width }, (_, index) => nativeCell(row.cells[index] || ''))),
|
||||
]
|
||||
const celldata = matrix.flatMap((row, r) => row.map((v, c) => ({ r, c, v })))
|
||||
const calcChain = celldata.flatMap(({ r, c, v }) => typeof v?.f === 'string' && v.f
|
||||
? [{ r, c, id: tableId }]
|
||||
: [])
|
||||
return [{
|
||||
name: title || '工作表 1',
|
||||
id: tableId,
|
||||
status: 1,
|
||||
row: Math.max(40, matrix.length + 10),
|
||||
column: Math.max(12, width + 4),
|
||||
celldata,
|
||||
// FortuneSheet requires formula cells in calcChain during initialization;
|
||||
// without it, the expression survives persistence but is not recalculated.
|
||||
calcChain,
|
||||
// FortuneSheet 1.0.4 creates `[0]` instead of `[0, 0]` for its first
|
||||
// automatic selection. Seed a complete A1 range for the location box,
|
||||
// formula editor and keyboard navigation.
|
||||
luckysheet_select_save: [{ row: [0, 0], column: [0, 0], row_focus: 0, column_focus: 0 }],
|
||||
}]
|
||||
}
|
||||
|
||||
function cellText(cell: FortuneCell | null | undefined): string {
|
||||
if (!cell) return ''
|
||||
if (typeof cell.f === 'string' && cell.f) return cell.f
|
||||
const value = cell.m ?? cell.v ?? ''
|
||||
return String(value)
|
||||
}
|
||||
|
||||
export function readWorkbookValue(
|
||||
sheet: FortuneSheet | undefined,
|
||||
previous: ChannelSpreadsheetValue,
|
||||
createId: () => string = () => crypto.randomUUID(),
|
||||
): ChannelSpreadsheetValue {
|
||||
const matrix = sheet?.data || []
|
||||
let lastRow = 0
|
||||
let lastColumn = Math.max(0, previous.columns.length - 1)
|
||||
matrix.forEach((row, rowIndex) => row?.forEach((cell, columnIndex) => {
|
||||
if (cellText(cell).trim()) {
|
||||
lastRow = Math.max(lastRow, rowIndex)
|
||||
lastColumn = Math.max(lastColumn, columnIndex)
|
||||
}
|
||||
}))
|
||||
const columnCount = Math.min(64, Math.max(1, lastColumn + 1))
|
||||
const columns = Array.from({ length: columnCount }, (_, index) => ({
|
||||
columnId: previous.columns[index]?.columnId || `COL-${createId()}`,
|
||||
title: cellText(matrix[0]?.[index]).trim() || previous.columns[index]?.title || `字段 ${index + 1}`,
|
||||
}))
|
||||
const rows = Array.from({ length: Math.min(5000, lastRow) }, (_, offset) => {
|
||||
const rowIndex = offset + 1
|
||||
return {
|
||||
rowId: previous.rows[offset]?.rowId || `ROW-${createId()}`,
|
||||
cells: Array.from({ length: columnCount }, (_, columnIndex) => cellText(matrix[rowIndex]?.[columnIndex])),
|
||||
}
|
||||
})
|
||||
return { columns, rows }
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
export const EDUCATION_EDITABLE_CELL_BUDGET = 240
|
||||
|
||||
export type EducationAdaptiveView = 'grid' | 'cards' | 'board' | 'dashboard'
|
||||
|
||||
export interface EducationAdaptiveInputs {
|
||||
rowCount: number
|
||||
columnCount: number
|
||||
numericColumnCount: number
|
||||
groupableColumnCount: number
|
||||
focusConfidence: 'high' | 'medium' | 'low'
|
||||
}
|
||||
|
||||
export interface EducationModelRenderProposal {
|
||||
primaryFocus: string
|
||||
groupColumnIndex?: number
|
||||
measureColumnIndex?: number
|
||||
views: EducationAdaptiveView[]
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export interface EducationAdaptiveRenderPlan {
|
||||
source: 'LOCAL_BASELINE' | 'MODEL_PROPOSAL'
|
||||
state: 'READY' | 'DEGRADED' | 'WAITING_HUMAN_CONFIRMATION' | 'MODEL_PROPOSAL_REJECTED'
|
||||
views: EducationAdaptiveView[]
|
||||
pageSize: number
|
||||
primaryFocus: string
|
||||
reasons: string[]
|
||||
}
|
||||
|
||||
export function educationEditablePageSize(columnCount: number): number {
|
||||
const boundedColumns = Math.max(1, Math.min(30, columnCount))
|
||||
return Math.max(5, Math.min(20, Math.floor(EDUCATION_EDITABLE_CELL_BUDGET / boundedColumns)))
|
||||
}
|
||||
|
||||
export function buildEducationBaselineRenderPlan(inputs: EducationAdaptiveInputs): EducationAdaptiveRenderPlan {
|
||||
const reasons: string[] = []
|
||||
const views: EducationAdaptiveView[] = ['grid', 'cards']
|
||||
if (inputs.groupableColumnCount > 0) views.push('board')
|
||||
else reasons.push('没有可靠分类字段,分类看板已降级停用。')
|
||||
if (inputs.numericColumnCount > 0) views.push('dashboard')
|
||||
else reasons.push('没有可靠数值字段,数据概览降级为计数摘要。')
|
||||
if (inputs.focusConfidence === 'low') reasons.push('重点置信度不足,保留人工选择,不自动突出字段。')
|
||||
if (inputs.rowCount * inputs.columnCount > EDUCATION_EDITABLE_CELL_BUDGET) reasons.push('可编辑单元格超过本机安全预算,已启用动态分页。')
|
||||
return {
|
||||
source: 'LOCAL_BASELINE',
|
||||
state: reasons.length ? 'DEGRADED' : 'READY',
|
||||
views,
|
||||
pageSize: educationEditablePageSize(inputs.columnCount),
|
||||
primaryFocus: inputs.focusConfidence === 'low' ? '重点待选择' : '本地基础算法重点',
|
||||
reasons,
|
||||
}
|
||||
}
|
||||
|
||||
export function validateEducationModelRenderProposal(
|
||||
inputs: EducationAdaptiveInputs,
|
||||
proposal: EducationModelRenderProposal,
|
||||
humanConfirmed: boolean,
|
||||
): EducationAdaptiveRenderPlan {
|
||||
const baseline = buildEducationBaselineRenderPlan(inputs)
|
||||
const reasons: string[] = []
|
||||
const uniqueViews = [...new Set(proposal.views)]
|
||||
if (!proposal.primaryFocus.trim() || proposal.primaryFocus.length > 120) reasons.push('模型建议的重点标题无效。')
|
||||
if (!uniqueViews.length || uniqueViews.some((view) => !['grid', 'cards', 'board', 'dashboard'].includes(view))) reasons.push('模型建议包含未登记视图。')
|
||||
if (proposal.groupColumnIndex !== undefined && (proposal.groupColumnIndex < 0 || proposal.groupColumnIndex >= inputs.columnCount)) reasons.push('模型建议的分类字段不存在。')
|
||||
if (proposal.measureColumnIndex !== undefined && (proposal.measureColumnIndex < 0 || proposal.measureColumnIndex >= inputs.columnCount)) reasons.push('模型建议的统计字段不存在。')
|
||||
const requestedPageSize = proposal.pageSize ?? baseline.pageSize
|
||||
const maximumSafePageSize = educationEditablePageSize(inputs.columnCount)
|
||||
if (!Number.isInteger(requestedPageSize) || requestedPageSize < 5 || requestedPageSize > maximumSafePageSize) reasons.push('模型建议的页面密度超过本机安全预算。')
|
||||
if (reasons.length) return { ...baseline, state: 'MODEL_PROPOSAL_REJECTED', reasons: [...reasons, '已优雅退回本地基础算法。'] }
|
||||
if (!humanConfirmed) return { ...baseline, state: 'WAITING_HUMAN_CONFIRMATION', reasons: ['模型建议已通过本地校验,等待用户确认后启用。'] }
|
||||
return {
|
||||
source: 'MODEL_PROPOSAL',
|
||||
state: 'READY',
|
||||
views: uniqueViews,
|
||||
pageSize: requestedPageSize,
|
||||
primaryFocus: proposal.primaryFocus.trim(),
|
||||
reasons: ['模型建议已通过本地结构校验、资源上限与用户确认。'],
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { analyzeEducationTableData } from './education-data'
|
||||
|
||||
describe('education data cleanup', () => {
|
||||
it('trims cells, removes only fully empty rows and keeps the first exact row', () => {
|
||||
const result = analyzeEducationTableData({
|
||||
columns: [
|
||||
{ columnId: 'name', title: '姓名' },
|
||||
{ columnId: 'course', title: '课程' },
|
||||
],
|
||||
rows: [
|
||||
{ rowId: 'row-1', cells: [' 冰朔 ', ' 写作 '] },
|
||||
{ rowId: 'row-2', cells: ['冰朔', '写作'] },
|
||||
{ rowId: 'row-3', cells: [' ', ''] },
|
||||
{ rowId: 'row-4', cells: ['另一位学员', ''] },
|
||||
],
|
||||
})
|
||||
|
||||
expect(result.originalRows).toBe(4)
|
||||
expect(result.emptyRows).toBe(1)
|
||||
expect(result.duplicateRows).toBe(1)
|
||||
expect(result.trimmedCells).toBe(3)
|
||||
expect(result.changeCount).toBe(5)
|
||||
expect(result.changePreview).toHaveLength(5)
|
||||
expect(result.cleanedRows).toEqual([
|
||||
{ rowId: 'row-1', cells: ['冰朔', '写作'] },
|
||||
{ rowId: 'row-4', cells: ['另一位学员', ''] },
|
||||
])
|
||||
})
|
||||
|
||||
it('does not merge rows that differ in any field', () => {
|
||||
const result = analyzeEducationTableData({
|
||||
columns: [{ columnId: 'status', title: '状态' }],
|
||||
rows: [
|
||||
{ rowId: 'row-1', cells: ['已完成'] },
|
||||
{ rowId: 'row-2', cells: ['进行中'] },
|
||||
],
|
||||
})
|
||||
|
||||
expect(result.duplicateRows).toBe(0)
|
||||
expect(result.cleanedRows).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('analyzes the entire table while keeping the human preview bounded', () => {
|
||||
const result = analyzeEducationTableData({
|
||||
columns: Array.from({ length: 23 }, (_, index) => ({ columnId: `column-${index}`, title: `字段 ${index + 1}` })),
|
||||
rows: Array.from({ length: 55 }, (_, rowIndex) => ({
|
||||
rowId: `row-${rowIndex}`,
|
||||
cells: Array.from({ length: 23 }, (_, columnIndex) => ` value-${rowIndex}-${columnIndex} `),
|
||||
})),
|
||||
})
|
||||
|
||||
expect(result.originalRows).toBe(55)
|
||||
expect(result.cleanedRows).toHaveLength(55)
|
||||
expect(result.trimmedCells).toBe(55 * 23)
|
||||
expect(result.changeCount).toBe(55 * 23)
|
||||
expect(result.changePreview).toHaveLength(12)
|
||||
expect(result.changePreview.every((change) => change.before.length <= 120 && change.after.length <= 120)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
export interface EducationDataColumn { columnId: string; title: string }
|
||||
export interface EducationDataRow { rowId: string; cells: string[] }
|
||||
export interface EducationDataTable { columns: EducationDataColumn[]; rows: EducationDataRow[] }
|
||||
|
||||
export const EDUCATION_CLEANUP_CHANGE_PREVIEW_LIMIT = 12
|
||||
|
||||
export type EducationDataChangeKind = 'TRIM_CELL' | 'REMOVE_EMPTY_ROW' | 'REMOVE_DUPLICATE_ROW'
|
||||
|
||||
export interface EducationDataChangePreview {
|
||||
kind: EducationDataChangeKind
|
||||
rowNumber: number
|
||||
columnTitle: string
|
||||
before: string
|
||||
after: string
|
||||
}
|
||||
|
||||
export interface EducationDataAnalysis {
|
||||
originalRows: number
|
||||
cleanedRows: EducationDataRow[]
|
||||
emptyRows: number
|
||||
duplicateRows: number
|
||||
trimmedCells: number
|
||||
changeCount: number
|
||||
changePreview: EducationDataChangePreview[]
|
||||
}
|
||||
|
||||
function previewCell(value: string): string {
|
||||
const singleLine = value.replace(/\t/g, '⇥').replace(/\r?\n/g, '↵').replace(/ /g, '·')
|
||||
return singleLine.length > 120 ? `${singleLine.slice(0, 117)}…` : singleLine
|
||||
}
|
||||
|
||||
export function analyzeEducationTableData(table: EducationDataTable): EducationDataAnalysis {
|
||||
let emptyRows = 0
|
||||
let duplicateRows = 0
|
||||
let trimmedCells = 0
|
||||
let changeCount = 0
|
||||
const seen = new Set<string>()
|
||||
const cleanedRows: EducationDataRow[] = []
|
||||
const changePreview: EducationDataChangePreview[] = []
|
||||
const addPreview = (change: EducationDataChangePreview) => {
|
||||
changeCount += 1
|
||||
if (changePreview.length < EDUCATION_CLEANUP_CHANGE_PREVIEW_LIMIT) changePreview.push(change)
|
||||
}
|
||||
for (const [rowIndex, row] of table.rows.entries()) {
|
||||
const cells = table.columns.map((_, index) => {
|
||||
const original = row.cells[index] || ''
|
||||
const cleaned = original.trim()
|
||||
if (cleaned !== original) {
|
||||
trimmedCells += 1
|
||||
addPreview({
|
||||
kind: 'TRIM_CELL',
|
||||
rowNumber: rowIndex + 1,
|
||||
columnTitle: table.columns[index]?.title || `字段 ${index + 1}`,
|
||||
before: previewCell(original),
|
||||
after: previewCell(cleaned),
|
||||
})
|
||||
}
|
||||
return cleaned
|
||||
})
|
||||
if (cells.every((cell) => !cell)) {
|
||||
emptyRows += 1
|
||||
addPreview({ kind: 'REMOVE_EMPTY_ROW', rowNumber: rowIndex + 1, columnTitle: '整行', before: '完整空行', after: '移除' })
|
||||
continue
|
||||
}
|
||||
const signature = JSON.stringify(cells)
|
||||
if (seen.has(signature)) {
|
||||
duplicateRows += 1
|
||||
addPreview({ kind: 'REMOVE_DUPLICATE_ROW', rowNumber: rowIndex + 1, columnTitle: '整行', before: '与前文完全重复', after: '移除' })
|
||||
continue
|
||||
}
|
||||
seen.add(signature)
|
||||
cleanedRows.push({ ...row, cells })
|
||||
}
|
||||
return { originalRows: table.rows.length, cleanedRows, emptyRows, duplicateRows, trimmedCells, changeCount, changePreview }
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import ChannelDocumentEngine from './channel-workbench/document-engine'
|
||||
|
||||
export default function EducationDocumentEngine({ body, onChange }: { body: string; onChange: (body: string) => void }) {
|
||||
return <ChannelDocumentEngine body={body} onChange={onChange} placeholder="从这里开始记录教学计划、会议纪要或课程资料……"/>
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
import type { CSSProperties } from 'react'
|
||||
export { nativeCompositionManifest } from './manifest'
|
||||
|
||||
export type CompositionDimension = 'SOURCE' | 'TOP_LEVEL_FOLDER'
|
||||
export type CompositionMeasure = 'DOCUMENT_COUNT' | 'TOTAL_BYTES' | 'DUPLICATE_COUNT'
|
||||
export type ProjectionView = 'DASHBOARD' | 'COMPARISON' | 'VERTICAL_BAR' | 'CLASSIFICATION' | 'TABLE'
|
||||
|
||||
export interface NativeCompositionRow {
|
||||
rowId: string
|
||||
source: string
|
||||
path: string
|
||||
title: string
|
||||
topLevelFolder: string
|
||||
sizeBytes: number
|
||||
duplicateCount: number
|
||||
updatedAtUnixMs: number
|
||||
}
|
||||
|
||||
export interface NativeCompositionGroup {
|
||||
key: string
|
||||
label: string
|
||||
documentCount: number
|
||||
totalBytes: number
|
||||
duplicateCount: number
|
||||
measureValue: number
|
||||
share: number
|
||||
}
|
||||
|
||||
export interface NativeCompositionProjection {
|
||||
schema: string
|
||||
state: string
|
||||
executionId: string
|
||||
executedAtUnixMs: number
|
||||
sourceIsRealAccountData: boolean
|
||||
readOnly: boolean
|
||||
dimension: CompositionDimension
|
||||
measure: CompositionMeasure
|
||||
views: ProjectionView[]
|
||||
nativeObject: {
|
||||
schema: string
|
||||
objectId: string
|
||||
title: string
|
||||
rowCount: number
|
||||
truncated: boolean
|
||||
sourceReceipt: string
|
||||
rows: NativeCompositionRow[]
|
||||
}
|
||||
groups: NativeCompositionGroup[]
|
||||
metrics: {
|
||||
rawDocumentCount: number
|
||||
uniqueDocumentCount: number
|
||||
duplicateDocumentCount: number
|
||||
totalBytes: number
|
||||
groupCount: number
|
||||
}
|
||||
recipe: {
|
||||
schema: string
|
||||
recipeId: string
|
||||
title: string
|
||||
nodes: { nodeId: string; moduleId: string }[]
|
||||
edges: { from: string; to: string }[]
|
||||
}
|
||||
dataSha256: string
|
||||
receiptSha256: string
|
||||
}
|
||||
|
||||
const viewNames: Record<ProjectionView, string> = {
|
||||
DASHBOARD: '仪表盘',
|
||||
COMPARISON: '对比',
|
||||
VERTICAL_BAR: '柱状图',
|
||||
CLASSIFICATION: '分类',
|
||||
TABLE: '明细表',
|
||||
}
|
||||
|
||||
const measureNames: Record<CompositionMeasure, string> = {
|
||||
DOCUMENT_COUNT: '文档数量',
|
||||
TOTAL_BYTES: '数据量',
|
||||
DUPLICATE_COUNT: '重复数量',
|
||||
}
|
||||
|
||||
export function formatCompositionValue(value: number, measure: CompositionMeasure): string {
|
||||
if (measure !== 'TOTAL_BYTES') return `${value.toLocaleString('zh-CN')} 篇`
|
||||
if (value < 1024) return `${value.toLocaleString('zh-CN')} B`
|
||||
if (value < 1024 * 1024) return `${(value / 1024).toFixed(value < 10 * 1024 ? 1 : 0)} KB`
|
||||
return `${(value / 1024 / 1024).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
export function toggleProjectionView(current: ProjectionView[], view: ProjectionView): ProjectionView[] {
|
||||
if (current.includes(view)) return current.length === 1 ? current : current.filter((item) => item !== view)
|
||||
return [...current, view]
|
||||
}
|
||||
|
||||
interface NativeCompositionStudioProps {
|
||||
projection: NativeCompositionProjection | null
|
||||
dimension: CompositionDimension
|
||||
measure: CompositionMeasure
|
||||
selectedViews: ProjectionView[]
|
||||
busy: boolean
|
||||
message: string
|
||||
onDimensionChange: (value: CompositionDimension) => void
|
||||
onMeasureChange: (value: CompositionMeasure) => void
|
||||
onViewsChange: (value: ProjectionView[]) => void
|
||||
onExecute: () => void
|
||||
}
|
||||
|
||||
export function NativeCompositionStudio(props: NativeCompositionStudioProps) {
|
||||
const { projection, dimension, measure, selectedViews, busy, message } = props
|
||||
const groups = projection?.groups || []
|
||||
const activeDimension = projection?.dimension || dimension
|
||||
const activeMeasure = projection?.measure || measure
|
||||
const activeViews = projection?.views || selectedViews
|
||||
const maximum = Math.max(1, ...groups.map((group) => group.measureValue))
|
||||
return <div className="native-composition-layout">
|
||||
<aside className="composition-recipe-panel">
|
||||
<header><span>原生组合配方</span><h2>知识结构观察</h2><p>同一份数据 · 多种动态投影</p></header>
|
||||
<section className="composition-source"><i/><div><span>真实数据源</span><b>当前账号知识目录</b><small>{projection ? `${projection.nativeObject.rowCount} 条原生对象` : '等待系统读取'}</small></div></section>
|
||||
<div className="composition-connector" aria-hidden="true"/>
|
||||
<label><span>分类维度</span><select aria-label="组合分类维度" value={dimension} onChange={(event) => props.onDimensionChange(event.target.value as CompositionDimension)}><option value="TOP_LEVEL_FOLDER">一级目录</option><option value="SOURCE">知识来源</option></select></label>
|
||||
<label><span>统计指标</span><select aria-label="组合统计指标" value={measure} onChange={(event) => props.onMeasureChange(event.target.value as CompositionMeasure)}><option value="DOCUMENT_COUNT">文档数量</option><option value="TOTAL_BYTES">数据量</option><option value="DUPLICATE_COUNT">重复数量</option></select></label>
|
||||
<fieldset><legend>人类投影</legend>{(Object.keys(viewNames) as ProjectionView[]).map((view) => <label key={view}><input type="checkbox" checked={selectedViews.includes(view)} onChange={() => props.onViewsChange(toggleProjectionView(selectedViews, view))}/><span>{viewNames[view]}</span></label>)}</fieldset>
|
||||
<button className="composition-execute" type="button" disabled={busy} onClick={props.onExecute}>{busy ? '系统正在组合…' : '按当前配方重新组合'}</button>
|
||||
<footer>{projection ? <><b>已由原生内核执行</b><span>回执 {projection.receiptSha256.slice(0, 12)}</span><small>{new Date(projection.executedAtUnixMs).toLocaleString('zh-CN')}</small></> : <span>{message || '组合执行只读,不修改知识原文。'}</span>}</footer>
|
||||
</aside>
|
||||
<main className="composition-projection-stage" aria-busy={busy}>
|
||||
<header><div><span>HUMAN PROJECTION</span><h1>知识空间结构组合</h1><p>{projection ? `按${activeDimension === 'SOURCE' ? '知识来源' : '一级目录'}观察${measureNames[activeMeasure]} · 数据来自当前账号真实知识目录` : '正在准备当前账号的原生知识对象'}</p></div><div className="composition-state"><i/><span>{projection?.sourceIsRealAccountData ? '真实数据已接入' : '等待数据'}</span><small>{projection?.readOnly ? '只读组合' : '未执行'}</small></div></header>
|
||||
{message && <p className="composition-message">{message}</p>}
|
||||
{projection && projection.nativeObject.rowCount === 0 && <section className="composition-empty"><span>空</span><h2>当前知识目录还没有内容</h2><p>这里不会用样例冒充真实数据。先在知识空间导入或建立页面,再重新组合。</p></section>}
|
||||
{projection && projection.nativeObject.rowCount > 0 && <div className="composition-view-grid">
|
||||
{activeViews.includes('DASHBOARD') && <section className="composition-view composition-dashboard"><header><span>仪表盘</span><small>同一执行结果的总览</small></header><div><article><span>唯一知识页</span><strong>{projection.metrics.uniqueDocumentCount.toLocaleString('zh-CN')}</strong><small>篇</small></article><article><span>原始页面</span><strong>{projection.metrics.rawDocumentCount.toLocaleString('zh-CN')}</strong><small>篇</small></article><article><span>知识数据量</span><strong>{formatCompositionValue(projection.metrics.totalBytes, 'TOTAL_BYTES')}</strong><small>原生目录</small></article><article><span>动态分类</span><strong>{projection.metrics.groupCount}</strong><small>组</small></article></div></section>}
|
||||
{activeViews.includes('VERTICAL_BAR') && <section className="composition-view composition-bars"><header><span>柱状图</span><small>{measureNames[activeMeasure]} · 动态比例</small></header><div className="composition-bar-plot">{groups.slice(0, 12).map((group) => <article key={group.key}><div className="composition-bar-track"><i style={{ '--bar-height': `${Math.max(4, group.measureValue / maximum * 100)}%` } as CSSProperties}/><em>{formatCompositionValue(group.measureValue, activeMeasure)}</em></div><b title={group.label}>{group.label}</b></article>)}</div></section>}
|
||||
{activeViews.includes('COMPARISON') && <section className="composition-view composition-comparison"><header><span>对比</span><small>各分类占比</small></header><div>{groups.slice(0, 10).map((group) => <article key={group.key}><div><b>{group.label}</b><span>{formatCompositionValue(group.measureValue, activeMeasure)}</span></div><i><em style={{ '--share': `${group.share * 100}%` } as CSSProperties}/></i><small>{(group.share * 100).toFixed(1)}%</small></article>)}</div></section>}
|
||||
{activeViews.includes('CLASSIFICATION') && <section className="composition-view composition-classification"><header><span>分类</span><small>从原生路径实时生成</small></header><div>{groups.map((group, index) => <article key={group.key}><i>{String(index + 1).padStart(2, '0')}</i><div><b>{group.label}</b><span>{group.documentCount} 篇文档 · {formatCompositionValue(group.totalBytes, 'TOTAL_BYTES')}</span></div><strong>{formatCompositionValue(group.measureValue, activeMeasure)}</strong></article>)}</div></section>}
|
||||
{activeViews.includes('TABLE') && <section className="composition-view composition-table"><header><span>原生明细表</span><small>{projection.nativeObject.schema} · 只读</small></header><div><table><thead><tr><th>标题</th><th>一级分类</th><th>来源</th><th>数据量</th><th>重复</th><th>更新时间</th></tr></thead><tbody>{projection.nativeObject.rows.slice(0, 100).map((row) => <tr key={row.rowId}><td title={row.path}>{row.title}</td><td>{row.topLevelFolder}</td><td>{row.source === 'native' ? '光湖原生' : row.source}</td><td>{formatCompositionValue(row.sizeBytes, 'TOTAL_BYTES')}</td><td>{row.duplicateCount}</td><td>{new Date(row.updatedAtUnixMs).toLocaleDateString('zh-CN')}</td></tr>)}</tbody></table></div></section>}
|
||||
</div>}
|
||||
</main>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
export const nativeCompositionManifest = {
|
||||
schema: 'hololake.composition-module/v1',
|
||||
moduleId: 'hololake.native-composition-projection',
|
||||
name: '原生组合视图',
|
||||
version: '0.1.0',
|
||||
slot: 'education-composition',
|
||||
origin: 'resident',
|
||||
input: 'hololake.human-projection/v1',
|
||||
exports: ['NativeCompositionStudio', 'formatCompositionValue', 'toggleProjectionView'],
|
||||
authority: 'READ_ONLY_HUMAN_PROJECTION',
|
||||
} as const
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { formatCompositionValue, toggleProjectionView, type ProjectionView } from './index'
|
||||
|
||||
describe('native composition human projection', () => {
|
||||
it('formats one native measure consistently across projections', () => {
|
||||
expect(formatCompositionValue(12, 'DOCUMENT_COUNT')).toBe('12 篇')
|
||||
expect(formatCompositionValue(1536, 'TOTAL_BYTES')).toBe('1.5 KB')
|
||||
expect(formatCompositionValue(2, 'DUPLICATE_COUNT')).toBe('2 篇')
|
||||
})
|
||||
|
||||
it('allows free projection combinations but never an empty projection', () => {
|
||||
const initial: ProjectionView[] = ['DASHBOARD', 'TABLE']
|
||||
expect(toggleProjectionView(initial, 'VERTICAL_BAR')).toEqual(['DASHBOARD', 'TABLE', 'VERTICAL_BAR'])
|
||||
expect(toggleProjectionView(initial, 'TABLE')).toEqual(['DASHBOARD'])
|
||||
expect(toggleProjectionView(['TABLE'], 'TABLE')).toEqual(['TABLE'])
|
||||
})
|
||||
})
|
||||
|
|
@ -147,7 +147,35 @@ svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-lineca
|
|||
.search-box input { min-width: 0; border: 0; outline: 0; color: var(--content-secondary); background: transparent; font-size: 14px; }
|
||||
.search-box button { border: 0; color: var(--content-muted); background: transparent; font-size: 12px; cursor: pointer; }
|
||||
.knowledge-counts { display: flex; justify-content: space-between; gap: 8px; padding: 13px 16px 10px; color: var(--content-muted); font-size: 11.5px; font-weight: 540; }
|
||||
.knowledge-tree { min-height: 0; overflow: auto; padding: 2px 8px 18px; }
|
||||
.knowledge-mode-switch { display: grid; grid-template-columns: 1fr 1fr; gap: 3px; margin: 10px 14px 0; padding: 3px; border: 1px solid var(--panel-edge); border-radius: 9px; background: color-mix(in srgb, var(--surface-depth) 68%, transparent); }
|
||||
.knowledge-mode-switch button { height: 30px; border: 0; border-radius: 6px; color: var(--content-muted); background: transparent; font-size: 12px; font-weight: 650; cursor: pointer; }
|
||||
.knowledge-mode-switch button.active { color: var(--content-primary); background: var(--primitive-glass-hover); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent-light) 20%, transparent); }
|
||||
.knowledge-organizer-overview { flex: 0 1 auto; max-height: 45%; overflow: auto; margin: 10px 10px 0; padding: 12px; border: 1px solid color-mix(in srgb, var(--panel-edge) 72%, transparent); border-radius: 11px; background: color-mix(in srgb, var(--primitive-glass) 42%, transparent); }
|
||||
.knowledge-organization-metrics { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
|
||||
.knowledge-organization-metrics span { display: grid; gap: 3px; padding: 8px 5px; border-radius: 8px; color: var(--content-muted); background: color-mix(in srgb, var(--surface-depth) 55%, transparent); text-align: center; font-size: 9.5px; }
|
||||
.knowledge-organization-metrics b { color: var(--content-primary); font-size: 16px; }
|
||||
.knowledge-organizer-overview section { margin-top: 14px; }
|
||||
.knowledge-organizer-overview section > header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 7px; }
|
||||
.knowledge-organizer-overview section > header b { color: var(--content-secondary); font-size: 11.5px; }
|
||||
.knowledge-organizer-overview section > header button { border: 0; color: var(--content-muted); background: transparent; font-size: 10px; cursor: pointer; }
|
||||
.knowledge-filter-results { padding: 9px; border: 1px solid color-mix(in srgb, var(--accent-light) 24%, var(--panel-edge)); border-radius: 9px; background: color-mix(in srgb, var(--accent-light) 5%, transparent); }
|
||||
.knowledge-filter-results > header span { color: var(--content-muted); font-size: 10px; }
|
||||
.knowledge-filter-results > div { display: grid; gap: 4px; }
|
||||
.knowledge-filter-results > div > button { display: grid; gap: 3px; width: 100%; padding: 8px 9px; border: 0; border-radius: 7px; color: var(--content-primary); background: transparent; text-align: left; cursor: pointer; }
|
||||
.knowledge-filter-results > div > button:hover { background: var(--primitive-glass-hover); }
|
||||
.knowledge-filter-results > div > button span { overflow: hidden; font-size: 11.5px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.knowledge-filter-results > div > button small { overflow: hidden; color: var(--content-muted); font-size: 9.5px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.knowledge-filter-results > p { margin: 7px 0 0; color: var(--content-muted); font-size: 9.5px; line-height: 1.45; }
|
||||
.knowledge-category-list { display: grid; gap: 3px; }
|
||||
.knowledge-category-list button { display: flex; align-items: center; justify-content: space-between; min-height: 30px; padding: 0 8px; border: 0; border-radius: 7px; color: var(--content-muted); background: transparent; font-size: 11.5px; text-align: left; cursor: pointer; }
|
||||
.knowledge-category-list button:hover, .knowledge-category-list button.active { color: var(--content-primary); background: var(--primitive-glass-hover); }
|
||||
.knowledge-category-list em { color: var(--content-muted); font-size: 10px; font-style: normal; }
|
||||
.knowledge-tag-cloud { display: flex; flex-wrap: wrap; gap: 5px; }
|
||||
.knowledge-tag-cloud button { display: flex; gap: 5px; align-items: center; padding: 5px 8px; border: 1px solid color-mix(in srgb, var(--panel-edge) 78%, transparent); border-radius: 999px; color: var(--content-muted); background: transparent; font-size: 10.5px; cursor: pointer; }
|
||||
.knowledge-tag-cloud button.active { border-color: color-mix(in srgb, var(--accent-light) 48%, transparent); color: var(--accent-light); background: color-mix(in srgb, var(--accent-light) 8%, transparent); }
|
||||
.knowledge-tag-cloud em { font-size: 9px; font-style: normal; opacity: .72; }
|
||||
.knowledge-tag-cloud p { margin: 0; color: var(--content-muted); font-size: 10.5px; line-height: 1.5; }
|
||||
.knowledge-tree { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 2px 8px 18px; }
|
||||
.tree-folder, .tree-document {
|
||||
width: 100%; min-height: 38px; display: grid; align-items: center; gap: 7px;
|
||||
border: 0; border-radius: 8px; color: var(--content-muted); background: transparent; text-align: left; cursor: pointer;
|
||||
|
|
@ -164,6 +192,7 @@ svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-lineca
|
|||
.tree-chevron.open { transform: rotate(90deg); }
|
||||
.knowledge-browser > footer, .code-channels > footer { min-height: 44px; padding: 12px 16px; border-top: 1px solid var(--panel-edge); color: var(--content-muted); font-size: 12px; line-height: 1.5; }
|
||||
.document-workspace, .code-reader { min-width: 0; min-height: 0; display: grid; grid-template-rows: 49px minmax(0, 1fr); }
|
||||
.document-workspace.knowledge-overview-open { grid-template-rows: minmax(0, 1fr); }
|
||||
.document-toolbar, .code-reader > header {
|
||||
min-width: 0; display: flex; align-items: center; justify-content: space-between; gap: 18px;
|
||||
padding: 0 20px; border-bottom: 1px solid var(--panel-edge); background: color-mix(in srgb, var(--surface-depth) 66%, transparent);
|
||||
|
|
@ -181,6 +210,33 @@ svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-lineca
|
|||
.reader-heading div { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 20px; }
|
||||
.reader-heading span { padding: 5px 8px; border-radius: 6px; color: var(--content-muted); background: var(--primitive-glass); font-size: 12px; }
|
||||
.reader-heading .meta-stats { margin-top: 15px; color: var(--content-muted); font-size: 12.5px; letter-spacing: .03em; }
|
||||
.reader-heading .reader-organization-line { margin: 0 0 12px; align-items: center; }
|
||||
.reader-heading .reader-organization-line span { padding: 0; color: var(--accent-light); background: transparent; font-size: 11px; font-weight: 700; letter-spacing: .08em; }
|
||||
.reader-heading .reader-organization-line em { padding: 2px 7px; border-radius: 999px; color: var(--content-muted); background: var(--primitive-glass); font-size: 9px; font-style: normal; }
|
||||
.knowledge-structure-overview { min-width: 0; min-height: 0; overflow: auto; padding: clamp(34px, 6vh, 70px) clamp(34px, 6vw, 88px) 80px; }
|
||||
.knowledge-structure-overview > header { max-width: 760px; }
|
||||
.knowledge-structure-overview > header > span { color: var(--accent-light); font-size: 10px; font-weight: 800; letter-spacing: .18em; }
|
||||
.knowledge-structure-overview h2 { margin: 9px 0 10px; color: var(--content-primary); font-size: clamp(30px, 3.4vw, 44px); font-weight: 720; letter-spacing: -.035em; }
|
||||
.knowledge-structure-overview > header p { margin: 0; color: var(--content-muted); font-size: 14px; font-weight: 520; line-height: 1.75; }
|
||||
.knowledge-structure-stats { display: grid; grid-template-columns: repeat(4, minmax(110px, 1fr)); gap: 10px; max-width: 820px; margin: 28px 0 34px; }
|
||||
.knowledge-structure-stats article { display: grid; gap: 5px; padding: 17px 18px; border: 1px solid var(--panel-edge); border-radius: 12px; background: var(--primitive-glass); }
|
||||
.knowledge-structure-stats b { color: var(--content-primary); font-size: 25px; font-weight: 700; }
|
||||
.knowledge-structure-stats span { color: var(--content-muted); font-size: 11px; }
|
||||
.knowledge-structure-overview > section { display: grid; gap: 6px; max-width: 820px; }
|
||||
.knowledge-structure-overview > section h3 { margin: 0 0 8px; color: var(--content-secondary); font-size: 13px; font-weight: 700; }
|
||||
.knowledge-structure-overview > section button { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 13px 15px; border: 0; border-bottom: 1px solid var(--panel-edge); color: var(--content-secondary); background: transparent; text-align: left; cursor: pointer; }
|
||||
.knowledge-structure-overview > section button:hover { border-radius: 9px; background: var(--primitive-glass-hover); }
|
||||
.knowledge-structure-overview > section button span { display: grid; gap: 4px; }
|
||||
.knowledge-structure-overview > section button b { color: var(--content-primary); font-size: 14px; }
|
||||
.knowledge-structure-overview > section button small { color: var(--content-muted); font-size: 10.5px; }
|
||||
.knowledge-structure-overview > section button strong { color: var(--accent-light); font-size: 16px; }
|
||||
.knowledge-structure-actions { display: flex; gap: 10px; margin-top: 28px; }
|
||||
.knowledge-organization-editor label { display: grid; gap: 5px; margin: 10px 0; }
|
||||
.knowledge-organization-editor label span { color: var(--content-muted); font-size: 10.5px; }
|
||||
.knowledge-organization-editor input { width: 100%; padding: 8px 9px; border: 1px solid var(--panel-edge); border-radius: 8px; outline: 0; color: var(--content-secondary); background: color-mix(in srgb, var(--surface-depth) 74%, transparent); font-size: 11px; }
|
||||
.knowledge-organization-editor input:focus { border-color: color-mix(in srgb, var(--accent-light) 46%, transparent); }
|
||||
.knowledge-organization-editor button { width: 100%; padding: 8px 10px; border: 1px solid color-mix(in srgb, var(--accent-light) 40%, transparent); border-radius: 8px; color: var(--content-primary); background: color-mix(in srgb, var(--accent-light) 8%, transparent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
||||
.knowledge-organization-editor button:disabled { opacity: .4; cursor: default; }
|
||||
.knowledge-tree .tree-branch { position: relative; }
|
||||
.tree-folder-tools { position: absolute; top: 4px; right: 6px; z-index: 5; }
|
||||
.tree-folder-menu-button { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border: 0; border-radius: 6px; background: transparent; color: var(--content-muted); cursor: pointer; opacity: .55; }
|
||||
|
|
@ -554,6 +610,7 @@ svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-lineca
|
|||
.world-title-actions { position: absolute; right: 16px; display: flex; align-items: center; gap: 15px; }
|
||||
.world-title-actions span { color: var(--content-muted); font-size: 12px; font-weight: 600; }
|
||||
.world-title-actions button { font-size: 12.5px; }
|
||||
.world-title-actions .main-world-return { color: var(--accent-light); }
|
||||
.world-scene { position: absolute; z-index: 5; inset: 44px 0 45px; min-height: 0; }
|
||||
.world-footer { position: absolute; z-index: 24; inset: auto 0 0; height: 45px; display: flex; align-items: center; justify-content: center; gap: 14px; }
|
||||
.world-footer b { color: var(--content-muted); font-size: 11.5px; font-weight: 600; letter-spacing: .11em; }
|
||||
|
|
@ -599,6 +656,7 @@ svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-lineca
|
|||
.official-hero h1 { margin: 0; color: var(--content-primary); font-size: clamp(24px, 3vw, 34px); font-weight: 700; letter-spacing: .09em; }
|
||||
.official-hero p { margin: 10px 0 0; color: var(--accent-light); font-size: 12px; font-weight: 600; letter-spacing: .31em; }
|
||||
.world-pool { position: absolute; z-index: 8; width: 190px; height: 100px; padding: 0; border: 0; background: transparent; cursor: pointer; color: inherit; }
|
||||
.world-pool:disabled { cursor: default; }
|
||||
.pool-bay { position: absolute; left: 50%; top: 0; width: 180px; height: 58px; transform: translateX(-50%); animation: world-pool-float 6.8s ease-in-out infinite; }
|
||||
@keyframes world-pool-float { 0%, 100% { translate: 0 0; } 50% { translate: 0 -9px; } }
|
||||
.pool-halo, .pool-heart, .pool-ring { position: absolute; left: 50%; top: 50%; border-radius: 50%; transform: translate(-50%, -50%); }
|
||||
|
|
@ -642,6 +700,8 @@ svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-lineca
|
|||
.nucleus-locus { position: absolute; inset: 0; width: 100%; display: grid; align-content: center; justify-items: center; gap: 18px; padding: 0; border: 0; background: transparent; cursor: pointer; transition: opacity .45s ease, transform .55s ease; }
|
||||
.nucleus-locus i { width: 250px; height: 88px; border-radius: 50%; filter: blur(7px); background: radial-gradient(closest-side, color-mix(in srgb, var(--primitive-warm-glow) 70%, transparent), color-mix(in srgb, var(--primitive-warm-glow) 22%, transparent) 52%, transparent 76%); animation: world-pool-breathe 5.2s ease-in-out infinite; }
|
||||
.nucleus-locus b { color: var(--content-secondary); font-size: 16px; font-weight: 700; letter-spacing: .13em; }
|
||||
.authenticated-world-entry .nucleus-locus { gap: 10px; }
|
||||
.authenticated-world-entry .nucleus-locus small { color: var(--content-muted); font-size: 12px; font-weight: 650; letter-spacing: .08em; }
|
||||
.number-nucleus.open .nucleus-locus { opacity: 0; transform: rotateX(58deg) translateY(-12px); pointer-events: none; }
|
||||
.nucleus-panel, .domain-credential { border: 1px solid var(--panel-edge); border-radius: 18px; background: color-mix(in srgb, var(--panel-bg) 64%, transparent); backdrop-filter: blur(22px); box-shadow: 0 26px 80px rgba(0, 0, 0, .48); }
|
||||
.nucleus-panel { position: absolute; left: 50%; top: 16px; width: 440px; padding: 27px 36px 25px; opacity: 0; transform: translateX(-50%) rotateX(-64deg); transform-origin: 50% 100%; pointer-events: none; transition: opacity .5s ease, transform .55s cubic-bezier(.22, 1, .36, 1); }
|
||||
|
|
@ -681,6 +741,409 @@ svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-lineca
|
|||
.channel-main .pool-bay { width: 260px; height: 84px; } .channel-main .pool-label { top: 92px; }
|
||||
.channel-knowledge { left: 14%; top: 48%; } .channel-code { left: 31%; top: 61%; } .channel-light { right: 31%; top: 61%; } .channel-status { right: 14%; top: 48%; }
|
||||
.channel-time { left: 50%; bottom: 4%; transform: translateX(-50%); }
|
||||
.subdomain-industry-entry .pool-label b { color: var(--accent-light); }
|
||||
.subdomain-industry-world { background: radial-gradient(circle at 50% 44%, color-mix(in srgb, var(--primitive-cool-glow) 10%, transparent), transparent 42%); }
|
||||
.public-industry-world { z-index: 24; background-color: color-mix(in srgb, var(--surface-depth) 96%, transparent); backdrop-filter: blur(20px); }
|
||||
.industry-pool { width: 240px; }
|
||||
.industry-pool .pool-bay { width: 220px; height: 72px; }
|
||||
.industry-pool .pool-label { top: 78px; }
|
||||
.industry-pool:disabled { opacity: .52; }
|
||||
.industry-pool:disabled .pool-heart { box-shadow: 0 0 18px color-mix(in srgb, var(--primitive-cool-glow) 24%, transparent); }
|
||||
.industry-web-novel { left: 12%; top: 47%; }
|
||||
.industry-pet { right: 12%; top: 47%; }
|
||||
.industry-education { left: 50%; top: 42%; width: 300px; transform: translateX(-50%); }
|
||||
.industry-education .pool-bay { width: 280px; height: 92px; }
|
||||
.industry-education .pool-label { top: 98px; }
|
||||
.education-channel-core { left: 50%; top: 34%; width: 320px; transform: translateX(-50%); }
|
||||
.education-channel-core .pool-bay { width: 300px; height: 92px; }
|
||||
.education-channel-core .pool-label { top: 98px; }
|
||||
.education-module-slot { opacity: .62; }
|
||||
.education-module-slot:not(:disabled) { opacity: 1; }
|
||||
.education-module-slot .pool-heart { width: 20px; height: 20px; }
|
||||
.education-module-slot .pool-label b { color: var(--content-secondary); }
|
||||
.education-module-1 { left: 9%; top: 48%; }
|
||||
.education-module-2 { left: 28%; top: 64%; }
|
||||
.education-module-3 { left: 50%; top: 72%; transform: translateX(-50%); }
|
||||
.education-module-4 { right: 28%; top: 64%; }
|
||||
.education-module-5 { right: 9%; top: 48%; }
|
||||
.education-channel-boundary { position: absolute; z-index: 10; left: 50%; bottom: 7%; width: min(820px, calc(100% - 72px)); margin: 0; transform: translateX(-50%); color: var(--content-muted); text-align: center; font-size: 12.5px; font-weight: 650; line-height: 1.75; letter-spacing: .055em; text-wrap: balance; }
|
||||
.education-workspace-world { position: absolute; z-index: 52; inset: 0; display: grid; grid-template-rows: 64px minmax(0, 1fr); color: var(--content-primary); background: radial-gradient(circle at 74% 16%, color-mix(in srgb, var(--primitive-cool-glow) 12%, transparent), transparent 32%), color-mix(in srgb, var(--surface-depth) 97%, transparent); backdrop-filter: blur(24px); }
|
||||
.education-workspace-bar { display: grid; grid-template-columns: minmax(160px, 1fr) auto minmax(160px, 1fr); align-items: center; gap: 18px; padding: 0 24px; border-bottom: 1px solid color-mix(in srgb, var(--panel-edge) 72%, transparent); background: color-mix(in srgb, var(--panel-bg) 84%, transparent); }
|
||||
.education-workspace-bar > button { justify-self: start; border: 0; background: transparent; color: var(--content-muted); font-size: 12.5px; font-weight: 650; cursor: pointer; }
|
||||
.education-workspace-bar > div { display: grid; justify-items: center; gap: 3px; }
|
||||
.education-workspace-bar > div span { color: var(--content-muted); font-size: 9.5px; font-weight: 750; letter-spacing: .2em; }
|
||||
.education-workspace-bar > div b { color: var(--content-primary); font-size: 17px; font-weight: 750; letter-spacing: .08em; }
|
||||
.education-workspace-bar > .education-create-control { justify-self: end; display: flex; grid-auto-flow: column; align-items: center; gap: 7px; }
|
||||
.education-create-control button { display: inline-flex; align-items: center; gap: 7px; padding: 9px 12px; border: 1px solid color-mix(in srgb, var(--panel-edge) 82%, transparent); border-radius: 10px; color: var(--content-secondary); background: color-mix(in srgb, var(--primitive-glass) 74%, transparent); font-size: 11.5px; font-weight: 680; cursor: pointer; }
|
||||
.education-create-control button svg { width: 14px; height: 14px; }
|
||||
.education-create-control select { max-width: 118px; padding: 8px 28px 8px 10px; border: 1px solid color-mix(in srgb, var(--panel-edge) 80%, transparent); border-radius: 9px; outline: 0; color: var(--content-secondary); background: color-mix(in srgb, var(--panel-bg) 94%, transparent); font-size: 11.5px; font-weight: 650; }
|
||||
.education-workspace-bar .education-primary-action { justify-self: end; padding: 9px 15px; border: 1px solid color-mix(in srgb, var(--accent-light) 42%, transparent); border-radius: 10px; color: var(--content-primary); background: color-mix(in srgb, var(--primitive-warm-glow) 12%, transparent); }
|
||||
.education-workspace-bar button:disabled { opacity: .45; cursor: default; }
|
||||
.education-import-receipt { position: absolute; z-index: 120; top: 76px; right: 24px; width: min(560px, calc(100% - 48px)); overflow: hidden; border: 1px solid color-mix(in srgb, var(--accent-light) 34%, var(--panel-edge)); border-radius: 15px; background: color-mix(in srgb, var(--panel-bg) 96%, transparent); box-shadow: 0 28px 80px rgba(0, 0, 0, .48); backdrop-filter: blur(26px); }
|
||||
.education-import-receipt > header { display: flex; align-items: center; justify-content: space-between; gap: 18px; padding: 16px 17px 13px; border-bottom: 1px solid color-mix(in srgb, var(--panel-edge) 64%, transparent); }
|
||||
.education-import-receipt > header div { display: grid; gap: 4px; }
|
||||
.education-import-receipt > header span { color: var(--accent-light); font-size: 9px; font-weight: 800; letter-spacing: .14em; }
|
||||
.education-import-receipt > header b { color: var(--content-primary); font-size: 14px; font-weight: 750; }
|
||||
.education-import-receipt > header button { width: 30px; height: 30px; border: 0; border-radius: 8px; color: var(--content-muted); background: transparent; font-size: 20px; cursor: pointer; }
|
||||
.education-import-metrics { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1px; background: color-mix(in srgb, var(--panel-edge) 50%, transparent); }
|
||||
.education-import-metrics span { display: grid; justify-items: center; gap: 2px; padding: 12px 8px; color: var(--content-muted); background: var(--panel-bg); font-size: 9.5px; font-weight: 650; }
|
||||
.education-import-metrics b { color: var(--content-primary); font-size: 18px; font-weight: 760; }
|
||||
.education-import-pages { max-height: 220px; overflow: auto; padding: 8px; }
|
||||
.education-import-pages button { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; align-items: center; gap: 10px; width: 100%; padding: 10px 9px; border: 0; border-radius: 8px; color: var(--content-secondary); background: transparent; text-align: left; cursor: pointer; }
|
||||
.education-import-pages button:hover { background: var(--primitive-glass-hover); }
|
||||
.education-import-pages b { overflow: hidden; color: var(--content-primary); font-size: 11.5px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.education-import-pages span, .education-import-pages em { color: var(--content-muted); font-size: 9.5px; font-style: normal; font-weight: 650; white-space: nowrap; }
|
||||
.education-import-pages em { color: var(--accent-light); }
|
||||
.education-import-receipt > footer { display: flex; justify-content: space-between; gap: 12px; padding: 10px 15px; border-top: 1px solid color-mix(in srgb, var(--panel-edge) 58%, transparent); color: var(--content-muted); font-size: 9px; font-weight: 600; }
|
||||
.education-import-receipt.needs-help { border-color: color-mix(in srgb, rgb(244, 190, 92) 45%, var(--panel-edge)); }
|
||||
.education-import-receipt.needs-help > p { margin: 0; padding: 16px 17px 12px; color: var(--content-secondary); font-size: 11.5px; font-weight: 600; line-height: 1.7; }
|
||||
.education-import-receipt.needs-help dl { display: grid; gap: 1px; margin: 0 16px; background: color-mix(in srgb, var(--panel-edge) 55%, transparent); }
|
||||
.education-import-receipt.needs-help dl div { display: flex; justify-content: space-between; gap: 16px; padding: 9px 10px; background: var(--panel-bg); font-size: 10px; }
|
||||
.education-import-receipt.needs-help dt { color: var(--content-muted); }
|
||||
.education-import-receipt.needs-help dd { margin: 0; color: var(--content-secondary); font-weight: 700; }
|
||||
.education-assistance-actions { display: flex; justify-content: flex-end; gap: 8px; padding: 14px 16px 16px; }
|
||||
.education-assistance-actions button { padding: 8px 11px; border: 1px solid color-mix(in srgb, var(--panel-edge) 78%, transparent); border-radius: 8px; color: var(--content-secondary); background: var(--primitive-glass-hover); font-size: 10.5px; font-weight: 680; cursor: pointer; }
|
||||
.education-workspace-layout { min-height: 0; display: grid; grid-template-columns: 280px minmax(0, 1fr); gap: 14px; padding: 14px; }
|
||||
.education-library, .education-document-workbench, .education-table-workbench, .education-data-workbench, .education-automation-workbench { min-width: 0; min-height: 0; border: 1px solid color-mix(in srgb, var(--panel-edge) 80%, transparent); border-radius: 16px; background: color-mix(in srgb, var(--panel-bg) 76%, transparent); box-shadow: 0 24px 70px rgba(0, 0, 0, .24); overflow: hidden; }
|
||||
.education-library { display: grid; grid-template-rows: auto auto minmax(0, 1fr) auto; }
|
||||
.education-library > header { display: flex; align-items: center; justify-content: space-between; gap: 18px; padding: 20px 18px 16px; }
|
||||
.education-library > header div { display: grid; gap: 5px; }
|
||||
.education-library > header span { color: var(--content-muted); font-size: 10px; font-weight: 700; letter-spacing: .12em; }
|
||||
.education-library > header h2 { margin: 0; color: var(--content-primary); font-size: 19px; font-weight: 750; }
|
||||
.education-library > header strong { display: grid; place-items: center; min-width: 38px; height: 38px; border-radius: 50%; color: var(--accent-light); background: color-mix(in srgb, var(--primitive-cool-glow) 11%, transparent); font-size: 14px; }
|
||||
.education-directory-search { display: flex; align-items: center; gap: 8px; margin: 0 12px 12px; padding: 8px 10px; border: 1px solid color-mix(in srgb, var(--panel-edge) 72%, transparent); border-radius: 9px; color: var(--content-muted); background: color-mix(in srgb, var(--primitive-glass) 58%, transparent); }
|
||||
.education-directory-search span { font-size: 15px; font-weight: 700; }
|
||||
.education-directory-search input { min-width: 0; width: 100%; border: 0; outline: 0; color: var(--content-primary); background: transparent; font-size: 11.5px; font-weight: 600; }
|
||||
.education-directory-search input::placeholder { color: var(--content-muted); }
|
||||
.education-library-list { min-height: 0; overflow: auto; padding: 0 9px 12px; }
|
||||
.education-library-list > button { display: grid; gap: 6px; width: 100%; padding: 13px 12px; border: 0; border-radius: 11px; background: transparent; color: var(--content-secondary); text-align: left; cursor: pointer; }
|
||||
.education-library-list > button:hover { background: color-mix(in srgb, var(--primitive-cool-glow) 7%, transparent); }
|
||||
.education-library-list > button.active { background: color-mix(in srgb, var(--primitive-cool-glow) 12%, transparent); box-shadow: inset 2px 0 0 var(--accent-light); }
|
||||
.education-library-list b { color: var(--content-primary); font-size: 13.5px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.education-library-list small { color: var(--content-muted); font-size: 10.5px; font-weight: 600; }
|
||||
.education-library > footer { min-height: 50px; padding: 13px 17px; border-top: 1px solid color-mix(in srgb, var(--panel-edge) 58%, transparent); color: var(--content-muted); font-size: 11px; font-weight: 600; line-height: 1.55; }
|
||||
.education-empty-list { padding: 26px 12px; color: var(--content-muted); text-align: center; font-size: 12px; }
|
||||
.education-document-workbench, .education-table-workbench { display: grid; grid-template-rows: auto minmax(0, 1fr); }
|
||||
.education-editor-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-height: 58px; padding: 10px 14px 10px 18px; border-bottom: 1px solid color-mix(in srgb, var(--panel-edge) 64%, transparent); }
|
||||
.education-editor-toolbar > input { min-width: 180px; flex: 1; border: 0; outline: 0; background: transparent; color: var(--content-primary); font-size: 18px; font-weight: 750; letter-spacing: .02em; }
|
||||
.education-editor-toolbar > div { display: flex; align-items: center; gap: 8px; }
|
||||
.education-editor-toolbar span { margin-right: 4px; color: var(--content-muted); font-size: 10.5px; font-weight: 650; white-space: nowrap; }
|
||||
.education-editor-toolbar button { padding: 8px 11px; border: 1px solid color-mix(in srgb, var(--panel-edge) 80%, transparent); border-radius: 8px; color: var(--content-secondary); background: color-mix(in srgb, var(--primitive-glass) 78%, transparent); font-size: 11.5px; font-weight: 650; cursor: pointer; }
|
||||
.education-editor-toolbar button.primary { border-color: color-mix(in srgb, var(--accent-light) 44%, transparent); color: var(--content-primary); background: color-mix(in srgb, var(--primitive-warm-glow) 14%, transparent); }
|
||||
.education-editor-toolbar button svg { width: 14px; height: 14px; vertical-align: -2px; }
|
||||
.education-editor-toolbar button:disabled { opacity: .42; cursor: default; }
|
||||
.education-document-toolbar-title { min-width: 0; flex: 1; display: grid !important; gap: 3px !important; }
|
||||
.education-document-toolbar-title b { overflow: hidden; color: var(--content-primary); font-size: 17px; font-weight: 750; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.education-document-toolbar-title small { color: var(--content-muted); font-size: 9.5px; font-weight: 600; }
|
||||
.education-settings { position: relative; display: inline-flex; }
|
||||
.education-settings > button { display: inline-flex; align-items: center; gap: 6px; }
|
||||
.education-settings-menu { position: absolute; z-index: 90; top: calc(100% + 7px); right: 0; display: flex; flex-direction: column; min-width: 210px; padding: 6px; border: 1px solid color-mix(in srgb, var(--panel-edge) 90%, transparent); border-radius: 11px; background: var(--panel-bg); box-shadow: 0 18px 44px rgba(0, 0, 0, .42); }
|
||||
.education-settings-menu button { display: flex; align-items: center; gap: 9px; width: 100%; padding: 9px 10px; border: 0; color: var(--content-secondary); background: transparent; text-align: left; white-space: nowrap; }
|
||||
.education-settings-menu button:hover { color: var(--content-primary); background: var(--primitive-glass-hover); }
|
||||
.education-settings-menu button.danger { color: rgb(251, 146, 160); }
|
||||
.education-settings-menu > span { display: block; height: 1px; margin: 5px 4px; background: var(--panel-edge); }
|
||||
.education-document-compose { min-height: 0; display: grid; grid-template-rows: auto minmax(0, 1fr) 35px; background: color-mix(in srgb, var(--surface-depth) 58%, transparent); }
|
||||
.education-format-toolbar { display: flex; align-items: center; gap: 5px; min-height: 43px; padding: 6px 16px; border-bottom: 1px solid color-mix(in srgb, var(--panel-edge) 60%, transparent); background: color-mix(in srgb, var(--primitive-glass) 48%, transparent); }
|
||||
.education-format-toolbar button { min-width: 42px; padding: 6px 9px; border: 1px solid color-mix(in srgb, var(--panel-edge) 72%, transparent); border-radius: 7px; color: var(--content-secondary); background: color-mix(in srgb, var(--panel-bg) 48%, transparent); font-size: 10.5px; font-weight: 650; cursor: pointer; }
|
||||
.education-format-toolbar button:hover { color: var(--content-primary); background: var(--primitive-glass-hover); }
|
||||
.education-format-toolbar span { margin-left: auto; color: var(--content-muted); font-size: 9.5px; font-weight: 600; }
|
||||
.education-engine-toolbar { display: flex; align-items: center; gap: 5px; min-height: 43px; padding: 6px 16px; border-bottom: 1px solid color-mix(in srgb, var(--panel-edge) 60%, transparent); background: color-mix(in srgb, var(--primitive-glass) 48%, transparent); }
|
||||
.education-engine-toolbar > span { margin-right: 8px; color: var(--accent-light); font-size: 9px; font-weight: 800; letter-spacing: .12em; }
|
||||
.education-engine-toolbar > i { flex: 1; }
|
||||
.education-engine-toolbar button { min-width: 42px; padding: 6px 9px; border: 1px solid color-mix(in srgb, var(--panel-edge) 72%, transparent); border-radius: 7px; color: var(--content-secondary); background: color-mix(in srgb, var(--panel-bg) 48%, transparent); font-size: 10.5px; font-weight: 650; cursor: pointer; }
|
||||
.education-engine-toolbar button:hover { color: var(--content-primary); background: var(--primitive-glass-hover); }
|
||||
.education-engine-loading { min-height: 240px; display: grid; place-items: center; color: var(--content-muted); background: color-mix(in srgb, var(--surface-depth) 58%, transparent); font-size: 12px; font-weight: 650; letter-spacing: .04em; }
|
||||
.education-engine-canvas { position: relative; min-height: 0; overflow: auto; background: color-mix(in srgb, var(--surface-depth) 58%, transparent); }
|
||||
.education-engine-editor { width: min(900px, calc(100% - 52px)); min-height: 100%; margin: 0 auto; padding: 42px clamp(34px, 6vw, 76px) 80px; border-right: 1px solid color-mix(in srgb, var(--panel-edge) 50%, transparent); border-left: 1px solid color-mix(in srgb, var(--panel-edge) 50%, transparent); outline: 0; color: var(--content-primary); background: color-mix(in srgb, var(--panel-bg) 72%, transparent); font: 500 15px/1.9 -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif; }
|
||||
.education-engine-placeholder { position: absolute; top: 43px; left: max(calc((100% - 900px) / 2 + clamp(34px, 6vw, 76px)), calc(26px + clamp(34px, 6vw, 76px))); color: var(--content-faint); font-size: 14px; pointer-events: none; }
|
||||
.education-engine-paragraph { margin: 0 0 12px; }
|
||||
.education-engine-heading-two { margin: 24px 0 10px; color: var(--content-primary); font-size: 22px; line-height: 1.4; }
|
||||
.education-engine-quote { margin: 15px 0; padding: 8px 14px; border-left: 3px solid color-mix(in srgb, var(--accent-light) 66%, transparent); color: var(--content-secondary); background: color-mix(in srgb, var(--primitive-cool-glow) 6%, transparent); }
|
||||
.education-engine-list { margin: 8px 0 14px; padding-left: 26px; }
|
||||
.education-engine-checklist { padding-left: 7px; list-style: none; }
|
||||
.education-engine-bold { font-weight: 780; }
|
||||
.education-engine-italic { font-style: italic; }
|
||||
.education-document-compose > textarea { width: min(900px, calc(100% - 52px)); height: 100%; min-height: 0; justify-self: center; resize: none; padding: 42px clamp(34px, 6vw, 76px) 80px; border: 0; border-right: 1px solid color-mix(in srgb, var(--panel-edge) 50%, transparent); border-left: 1px solid color-mix(in srgb, var(--panel-edge) 50%, transparent); outline: 0; color: var(--content-primary); background: color-mix(in srgb, var(--panel-bg) 72%, transparent); font: 500 15px/1.9 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
||||
.education-document-compose > footer { display: flex; align-items: center; justify-content: space-between; padding: 0 18px; border-top: 1px solid color-mix(in srgb, var(--panel-edge) 56%, transparent); color: var(--content-muted); font-size: 9.5px; font-weight: 600; }
|
||||
.education-document-reading { min-height: 0; overflow: auto; padding: 28px 30px 80px; background: color-mix(in srgb, var(--surface-depth) 58%, transparent); }
|
||||
.education-document-paper { width: min(900px, 100%); min-height: calc(100% - 8px); margin: 0 auto; padding: clamp(44px, 7vw, 78px) clamp(40px, 8vw, 96px) 96px; border: 1px solid color-mix(in srgb, var(--panel-edge) 68%, transparent); border-radius: 14px; background: color-mix(in srgb, var(--panel-bg) 82%, transparent); box-shadow: 0 26px 64px rgba(0, 0, 0, .18); }
|
||||
.education-document-paper > header { margin: 0 0 26px; padding-bottom: 22px; border-bottom: 1px solid color-mix(in srgb, var(--panel-edge) 66%, transparent); }
|
||||
.education-document-paper > header span { color: var(--accent-light); font-size: 9.5px; font-weight: 800; letter-spacing: .16em; }
|
||||
.education-document-paper > header h1 { margin: 8px 0 10px; color: var(--content-primary); font-size: clamp(30px, 4vw, 46px); font-weight: 730; letter-spacing: -.035em; line-height: 1.16; }
|
||||
.education-document-paper > header p { margin: 0; color: var(--content-muted); font-size: 10.5px; font-weight: 600; }
|
||||
.education-document-paper > .markdown-document { max-width: none; padding: 0; font-size: 16px; }
|
||||
.education-document-split { min-height: 0; display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
|
||||
.education-document-editor, .education-document-preview { min-width: 0; min-height: 0; display: grid; grid-template-rows: 38px minmax(0, 1fr); }
|
||||
.education-document-editor { border-right: 1px solid color-mix(in srgb, var(--panel-edge) 55%, transparent); }
|
||||
.education-document-editor > header, .education-document-preview > header { display: flex; align-items: center; justify-content: space-between; padding: 0 16px; color: var(--content-muted); background: color-mix(in srgb, var(--primitive-glass) 44%, transparent); }
|
||||
.education-document-editor > header b, .education-document-preview > header b { color: var(--content-secondary); font-size: 11px; font-weight: 700; }
|
||||
.education-document-editor > header span, .education-document-preview > header span { font-size: 9.5px; font-weight: 650; letter-spacing: .09em; }
|
||||
.education-document-editor textarea { width: 100%; height: 100%; resize: none; padding: 24px; border: 0; outline: 0; color: var(--content-primary); background: transparent; font: 500 14px/1.8 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
||||
.education-document-preview > div { min-height: 0; overflow: auto; padding: 24px 30px 54px; }
|
||||
.education-workspace-empty { align-self: center; justify-self: center; display: grid; justify-items: center; width: min(480px, calc(100% - 48px)); text-align: center; }
|
||||
.education-workspace-empty > span { display: grid; place-items: center; width: 70px; height: 70px; border-radius: 50%; color: var(--accent-light); background: radial-gradient(circle, color-mix(in srgb, var(--primitive-warm-glow) 28%, transparent), transparent 72%); font-size: 25px; font-weight: 750; }
|
||||
.education-workspace-empty h2 { margin: 18px 0 8px; color: var(--content-primary); font-size: 24px; font-weight: 750; }
|
||||
.education-workspace-empty p { margin: 0; color: var(--content-muted); font-size: 13px; font-weight: 600; line-height: 1.7; }
|
||||
.education-workspace-empty button { margin-top: 20px; padding: 10px 18px; border: 1px solid color-mix(in srgb, var(--accent-light) 44%, transparent); border-radius: 10px; color: var(--content-primary); background: color-mix(in srgb, var(--primitive-warm-glow) 12%, transparent); cursor: pointer; }
|
||||
.education-template-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; margin-top: 20px; }
|
||||
.education-template-actions button { margin-top: 0; }
|
||||
.education-table-workbench { grid-template-rows: auto auto minmax(0, 1fr) auto; }
|
||||
.education-table-viewbar { display: flex; flex-wrap: wrap; align-items: end; gap: 8px; padding: 10px 14px; border-bottom: 1px solid color-mix(in srgb, var(--panel-edge) 56%, transparent); background: color-mix(in srgb, var(--primitive-glass) 38%, transparent); }
|
||||
.education-table-viewbar label { display: grid; flex: 0 1 170px; gap: 5px; }
|
||||
.education-table-viewbar > label:first-child { flex: 1 1 220px; }
|
||||
.education-table-viewbar label > span { color: var(--content-muted); font-size: 9px; font-weight: 700; letter-spacing: .08em; }
|
||||
.education-table-viewbar input, .education-table-viewbar select { min-width: 0; height: 32px; padding: 0 10px; border: 1px solid color-mix(in srgb, var(--panel-edge) 76%, transparent); border-radius: 8px; outline: 0; color: var(--content-primary); background: color-mix(in srgb, var(--panel-bg) 82%, transparent); font-size: 11px; font-weight: 600; }
|
||||
.education-view-switch { display: flex; flex-wrap: wrap; padding: 3px; border: 1px solid color-mix(in srgb, var(--panel-edge) 70%, transparent); border-radius: 9px; background: color-mix(in srgb, var(--panel-bg) 70%, transparent); }
|
||||
.education-view-switch button, .education-sort-direction { height: 32px; padding: 0 11px; border: 0; border-radius: 7px; color: var(--content-muted); background: transparent; font-size: 10.5px; font-weight: 700; cursor: pointer; }
|
||||
.education-view-switch button.active { color: var(--content-primary); background: color-mix(in srgb, var(--primitive-cool-glow) 14%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent-light) 25%, transparent); }
|
||||
.education-sort-direction { border: 1px solid color-mix(in srgb, var(--panel-edge) 76%, transparent); background: color-mix(in srgb, var(--panel-bg) 72%, transparent); }
|
||||
.education-sensitive-toggle { height: 32px; padding: 0 11px; border: 1px solid color-mix(in srgb, var(--accent-light) 34%, var(--panel-edge)); border-radius: 7px; color: var(--content-secondary); background: color-mix(in srgb, var(--primitive-warm-glow) 8%, var(--panel-bg)); font-size: 10.5px; font-weight: 700; cursor: pointer; }
|
||||
.education-sensitive-masked { -webkit-text-security: disc; }
|
||||
.education-grid-scroll { position: relative; min-height: 0; overflow: auto; }
|
||||
.channel-spreadsheet-engine { min-width: 0; min-height: 0; display: grid; grid-template-rows: 42px minmax(0, 1fr); overflow: hidden; background: #f6f7f9; }
|
||||
.channel-spreadsheet-engine > header { display: flex; align-items: center; justify-content: space-between; gap: 18px; padding: 0 14px; border-bottom: 1px solid #d9dde4; color: #202632; background: #f8f9fb; }
|
||||
.channel-spreadsheet-engine > header div { min-width: 0; display: flex; align-items: baseline; gap: 10px; }
|
||||
.channel-spreadsheet-engine > header b { font-size: 12.5px; font-weight: 760; }
|
||||
.channel-spreadsheet-engine > header span, .channel-spreadsheet-engine > header em { overflow: hidden; color: #667085; text-overflow: ellipsis; white-space: nowrap; font-size: 9.5px; font-style: normal; font-weight: 620; }
|
||||
.channel-spreadsheet-canvas { min-width: 0; min-height: 0; overflow: hidden; color: #202632; background: #fff; }
|
||||
.channel-spreadsheet-canvas .fortune-container { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif; }
|
||||
.channel-spreadsheet-canvas .fortune-toolbar { border-bottom-color: #d9dde4; }
|
||||
.channel-spreadsheet-canvas .fortune-toolbar-button:hover, .channel-spreadsheet-canvas .fortune-toolbar-combo:hover { background: #edf1f6; }
|
||||
.education-workbook-privacy, .education-engine-loading { min-height: 0; display: grid; place-content: center; justify-items: center; gap: 9px; padding: 40px; color: var(--content-secondary); text-align: center; }
|
||||
.education-workbook-privacy b { color: var(--content-primary); font-size: 16px; }
|
||||
.education-workbook-privacy p { max-width: 560px; margin: 0; color: var(--content-muted); font-size: 11px; font-weight: 620; line-height: 1.7; }
|
||||
.education-workbook-privacy button { padding: 9px 13px; border: 1px solid color-mix(in srgb, var(--accent-light) 40%, transparent); border-radius: 9px; color: var(--content-primary); background: color-mix(in srgb, var(--primitive-warm-glow) 12%, transparent); font-size: 11px; font-weight: 700; cursor: pointer; }
|
||||
.education-data-grid { width: max-content; min-width: 100%; border-collapse: separate; border-spacing: 0; table-layout: fixed; }
|
||||
.education-data-grid th, .education-data-grid td { width: 148px; min-width: 148px; max-width: 148px; border-right: 1px solid color-mix(in srgb, var(--panel-edge) 48%, transparent); border-bottom: 1px solid color-mix(in srgb, var(--panel-edge) 48%, transparent); background: color-mix(in srgb, var(--panel-bg) 31%, transparent); }
|
||||
.education-data-grid thead th { position: sticky; z-index: 3; top: 0; height: 46px; background: color-mix(in srgb, var(--panel-bg) 96%, transparent); }
|
||||
.education-data-grid th > div { display: flex; align-items: center; }
|
||||
.education-data-grid input { width: 100%; min-width: 0; padding: 9px 10px; border: 0; outline: 0; color: var(--content-secondary); background: transparent; font-size: 11.5px; font-weight: 580; text-overflow: ellipsis; }
|
||||
.education-data-grid th input { font-weight: 750; }
|
||||
.education-data-grid input:focus { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent-light) 60%, transparent); background: color-mix(in srgb, var(--primitive-cool-glow) 7%, transparent); }
|
||||
.education-data-grid tbody tr:nth-child(even) td { background: color-mix(in srgb, var(--primitive-glass) 46%, var(--panel-bg)); }
|
||||
.education-data-grid tbody tr:hover td { background: color-mix(in srgb, var(--primitive-cool-glow) 7%, var(--panel-bg)); }
|
||||
.education-data-grid th button, .education-row-action button { width: 30px; border: 0; background: transparent; color: var(--content-muted); font-size: 17px; cursor: pointer; }
|
||||
.education-data-grid th button:disabled { opacity: .28; cursor: default; }
|
||||
.education-data-grid .education-row-index { position: sticky; z-index: 2; left: 0; min-width: 48px; width: 48px; color: var(--content-muted); background: color-mix(in srgb, var(--panel-bg) 96%, transparent); text-align: center; font-size: 10.5px; font-weight: 700; }
|
||||
.education-data-grid thead .education-row-index { z-index: 4; }
|
||||
.education-data-grid thead th:nth-child(2) { position: sticky; z-index: 4; left: 48px; box-shadow: 8px 0 18px rgba(0, 0, 0, .18); }
|
||||
.education-data-grid tbody td:nth-child(2) { position: sticky; z-index: 1; left: 48px; box-shadow: 8px 0 18px rgba(0, 0, 0, .12); }
|
||||
.education-data-grid .education-row-action { min-width: 42px; width: 42px; text-align: center; }
|
||||
.education-grid-empty { display: grid; justify-items: center; padding: 70px 20px; color: var(--content-muted); font-size: 12.5px; }
|
||||
.education-grid-empty button { padding: 8px 13px; border: 1px solid var(--panel-edge); border-radius: 8px; color: var(--content-secondary); background: transparent; cursor: pointer; }
|
||||
.education-card-view { min-height: 0; overflow: auto; display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); align-content: start; gap: 12px; padding: 14px; }
|
||||
.education-card-view article { min-width: 0; display: grid; gap: 12px; padding: 15px; border: 1px solid color-mix(in srgb, var(--panel-edge) 72%, transparent); border-radius: 13px; background: color-mix(in srgb, var(--primitive-glass) 62%, transparent); box-shadow: 0 14px 34px rgba(0, 0, 0, .12); }
|
||||
.education-card-view article > header { display: grid; grid-template-columns: 24px minmax(0, 1fr) 24px; align-items: center; gap: 8px; }
|
||||
.education-card-view article > header span { color: var(--accent-light); font-size: 10px; font-weight: 800; }
|
||||
.education-card-view article > header input { min-width: 0; border: 0; border-bottom: 1px solid color-mix(in srgb, var(--panel-edge) 60%, transparent); outline: 0; color: var(--content-primary); background: transparent; font-size: 14px; font-weight: 750; }
|
||||
.education-card-view article > header button { border: 0; color: var(--content-muted); background: transparent; font-size: 16px; cursor: pointer; }
|
||||
.education-card-view article > label { display: grid; gap: 5px; }
|
||||
.education-card-view article > label span { color: var(--content-muted); font-size: 9.5px; font-weight: 700; letter-spacing: .05em; }
|
||||
.education-card-view article > label input { min-height: 38px; padding: 8px 9px; border: 1px solid color-mix(in srgb, var(--panel-edge) 62%, transparent); border-radius: 8px; outline: 0; color: var(--content-secondary); background: color-mix(in srgb, var(--panel-bg) 48%, transparent); font: 600 11.5px/1.55 inherit; }
|
||||
.education-card-view article textarea { min-height: 54px; resize: vertical; padding: 8px 9px; border: 1px solid color-mix(in srgb, var(--panel-edge) 62%, transparent); border-radius: 8px; outline: 0; color: var(--content-secondary); background: color-mix(in srgb, var(--panel-bg) 48%, transparent); font: 600 11.5px/1.55 inherit; }
|
||||
.education-board-view { min-height: 0; overflow: auto; display: flex; align-items: flex-start; gap: 12px; padding: 14px; }
|
||||
.education-board-view > section { flex: 0 0 286px; max-height: 100%; overflow: hidden; display: grid; grid-template-rows: auto minmax(0, 1fr); border: 1px solid color-mix(in srgb, var(--panel-edge) 70%, transparent); border-radius: 13px; background: color-mix(in srgb, var(--primitive-glass) 52%, transparent); }
|
||||
.education-board-view > section > header { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 12px 13px; border-bottom: 1px solid color-mix(in srgb, var(--panel-edge) 56%, transparent); }
|
||||
.education-board-view > section > header div { display: grid; gap: 2px; min-width: 0; }
|
||||
.education-board-view > section > header b { overflow: hidden; color: var(--content-primary); font-size: 12.5px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.education-board-view > section > header span, .education-board-view > section > header em { color: var(--content-muted); font-size: 9.5px; font-style: normal; font-weight: 650; }
|
||||
.education-board-view > section > div { min-height: 0; overflow: auto; display: grid; align-content: start; gap: 8px; padding: 9px; }
|
||||
.education-board-view article { display: grid; gap: 8px; padding: 11px; border: 1px solid color-mix(in srgb, var(--panel-edge) 58%, transparent); border-radius: 10px; background: color-mix(in srgb, var(--panel-bg) 78%, transparent); }
|
||||
.education-board-view article p { display: grid; grid-template-columns: 72px minmax(0, 1fr); gap: 8px; margin: 0; }
|
||||
.education-board-view article p span { overflow: hidden; color: var(--content-muted); font-size: 9px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.education-board-view article p b { overflow: hidden; color: var(--content-secondary); font-size: 10.5px; font-weight: 650; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.education-board-view article button { justify-self: end; padding: 5px 8px; border: 0; color: var(--accent-light); background: transparent; font-size: 9.5px; font-weight: 700; cursor: pointer; }
|
||||
.education-dashboard-view { min-height: 0; overflow: auto; display: grid; align-content: start; gap: 14px; padding: 18px; }
|
||||
.education-focus-hero { display: grid; grid-template-columns: minmax(0, 1fr) minmax(240px, .42fr); gap: 22px; align-items: center; padding: 22px 24px; border: 1px solid color-mix(in srgb, var(--primitive-warm-glow) 32%, var(--panel-edge)); border-radius: 16px; background: radial-gradient(80% 160% at 100% 50%, color-mix(in srgb, var(--primitive-warm-glow) 15%, transparent), transparent 68%), color-mix(in srgb, var(--panel-bg) 72%, transparent); }
|
||||
.education-focus-hero > div > span { color: var(--accent-light); font-size: 9.5px; font-weight: 800; letter-spacing: .14em; }
|
||||
.education-focus-hero h2 { margin: 7px 0 5px; color: var(--content-primary); font-size: clamp(24px, 3vw, 34px); font-weight: 780; }
|
||||
.education-focus-hero p { margin: 0; color: var(--content-muted); font-size: 11px; font-weight: 600; line-height: 1.7; }
|
||||
.education-focus-hero > article { display: grid; justify-items: end; gap: 4px; }
|
||||
.education-focus-hero > article > span { color: var(--content-muted); font-size: 9.5px; font-weight: 700; }
|
||||
.education-focus-hero > article > b { color: var(--accent-light); font-size: clamp(30px, 4vw, 46px); font-weight: 780; letter-spacing: -.04em; }
|
||||
.education-focus-hero > article > div { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 10px; }
|
||||
.education-focus-hero > article em { color: var(--content-muted); font-size: 9.5px; font-style: normal; font-weight: 650; }
|
||||
.education-focus-hero.needs-selection { border-style: dashed; }
|
||||
.education-model-mode-banner { display: flex; align-items: center; justify-content: space-between; gap: 18px; padding: 11px 13px; border: 1px solid color-mix(in srgb, var(--panel-edge) 56%, transparent); border-radius: 10px; background: color-mix(in srgb, var(--primitive-glass) 42%, transparent); }
|
||||
.education-model-mode-banner div { display: grid; gap: 3px; }
|
||||
.education-model-mode-banner b { color: var(--content-secondary); font-size: 10.5px; }
|
||||
.education-model-mode-banner span { color: var(--content-muted); font-size: 9.5px; line-height: 1.5; }
|
||||
.education-model-mode-banner button { flex: 0 0 auto; padding: 7px 10px; border: 1px solid color-mix(in srgb, var(--accent-light) 32%, var(--panel-edge)); border-radius: 8px; color: var(--accent-light); background: transparent; font-size: 9.5px; font-weight: 700; cursor: pointer; }
|
||||
.education-dashboard-metrics { display: grid; grid-template-columns: repeat(4, minmax(140px, 1fr)); gap: 10px; }
|
||||
.education-dashboard-metrics article { display: grid; gap: 5px; padding: 15px; border: 1px solid color-mix(in srgb, var(--panel-edge) 66%, transparent); border-radius: 12px; background: color-mix(in srgb, var(--primitive-glass) 58%, transparent); }
|
||||
.education-dashboard-metrics span { color: var(--content-muted); font-size: 9.5px; font-weight: 750; letter-spacing: .08em; }
|
||||
.education-dashboard-metrics b { color: var(--content-primary); font-size: 25px; font-weight: 760; }
|
||||
.education-dashboard-metrics em { overflow: hidden; color: var(--content-muted); font-size: 9px; font-style: normal; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.education-dashboard-chart { display: grid; gap: 14px; padding: 17px; border: 1px solid color-mix(in srgb, var(--panel-edge) 68%, transparent); border-radius: 14px; background: color-mix(in srgb, var(--panel-bg) 55%, transparent); }
|
||||
.education-dashboard-chart > header { display: flex; align-items: end; justify-content: space-between; gap: 18px; }
|
||||
.education-dashboard-chart > header div { display: grid; gap: 4px; }
|
||||
.education-dashboard-chart > header span { color: var(--accent-light); font-size: 9px; font-weight: 800; letter-spacing: .12em; }
|
||||
.education-dashboard-chart h3 { margin: 0; color: var(--content-primary); font-size: 16px; }
|
||||
.education-dashboard-chart > header > b { color: var(--content-muted); font-size: 10px; }
|
||||
.education-dashboard-chart > div { display: grid; gap: 9px; }
|
||||
.education-dashboard-chart article { display: grid; grid-template-columns: minmax(90px, 150px) minmax(160px, 1fr) 34px minmax(120px, auto); align-items: center; gap: 10px; }
|
||||
.education-dashboard-chart article > span { overflow: hidden; color: var(--content-secondary); font-size: 10.5px; font-weight: 650; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.education-dashboard-chart article > div { overflow: hidden; height: 9px; border-radius: 999px; background: color-mix(in srgb, var(--panel-edge) 62%, transparent); }
|
||||
.education-dashboard-chart article i { display: block; height: 100%; border-radius: inherit; background: linear-gradient(90deg, color-mix(in srgb, var(--accent-light) 70%, transparent), color-mix(in srgb, var(--primitive-cool-glow) 70%, var(--accent-light))); }
|
||||
.education-dashboard-chart article b { color: var(--content-primary); font-size: 10.5px; text-align: right; }
|
||||
.education-dashboard-chart article em { overflow: hidden; color: var(--content-muted); font-size: 9.5px; font-style: normal; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.education-dashboard-view > footer { color: var(--content-muted); font-size: 9.5px; font-weight: 600; text-align: right; }
|
||||
.education-table-status { display: flex; justify-content: space-between; gap: 18px; padding: 10px 16px; border-top: 1px solid color-mix(in srgb, var(--panel-edge) 58%, transparent); color: var(--content-muted); font-size: 10.5px; font-weight: 650; }
|
||||
.education-degradation-receipt { color: var(--accent-light); cursor: help; }
|
||||
.education-table-pagination { display: flex; align-items: center; gap: 8px; }
|
||||
.education-table-pagination b { color: var(--content-secondary); font-size: 10.5px; font-weight: 700; }
|
||||
.education-table-pagination button { padding: 4px 9px; border: 1px solid color-mix(in srgb, var(--panel-edge) 75%, transparent); border-radius: 6px; color: var(--content-secondary); background: color-mix(in srgb, var(--panel-bg) 74%, transparent); font-size: 10px; cursor: pointer; }
|
||||
.education-table-pagination button:disabled { opacity: .38; cursor: default; }
|
||||
.education-data-workbench { overflow: auto; padding: 20px; }
|
||||
.education-data-workbench > header { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 2px 2px 18px; }
|
||||
.education-data-workbench > header div { display: grid; gap: 5px; }
|
||||
.education-data-workbench > header span { color: var(--content-muted); font-size: 9.5px; font-weight: 750; letter-spacing: .12em; }
|
||||
.education-data-workbench > header h2 { margin: 0; color: var(--content-primary); font-size: 22px; font-weight: 760; }
|
||||
.education-data-workbench > header > b { color: var(--accent-light); font-size: 11px; font-weight: 750; }
|
||||
.education-cleanup-metrics { display: grid; grid-template-columns: repeat(5, minmax(110px, 1fr)); gap: 10px; }
|
||||
.education-cleanup-metrics article { display: grid; grid-template-columns: 1fr auto; align-items: end; gap: 4px 10px; padding: 14px; border: 1px solid color-mix(in srgb, var(--panel-edge) 70%, transparent); border-radius: 12px; background: color-mix(in srgb, var(--primitive-glass) 60%, transparent); }
|
||||
.education-cleanup-metrics article span { grid-column: 1 / -1; color: var(--content-muted); font-size: 9.5px; font-weight: 700; }
|
||||
.education-cleanup-metrics article strong { color: var(--content-primary); font-size: 25px; font-weight: 760; }
|
||||
.education-cleanup-metrics article small { padding-bottom: 3px; color: var(--content-muted); font-size: 9.5px; font-weight: 650; }
|
||||
.education-cleanup-rules { margin-top: 13px; padding: 16px 18px; border: 1px solid color-mix(in srgb, var(--panel-edge) 68%, transparent); border-radius: 13px; background: color-mix(in srgb, var(--primitive-glass) 44%, transparent); }
|
||||
.education-cleanup-rules h3, .education-cleanup-preview h3 { margin: 0; color: var(--content-primary); font-size: 14px; font-weight: 750; }
|
||||
.education-cleanup-rules ol { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; margin: 14px 0 0; padding: 0; list-style: none; counter-reset: cleanup-rule; }
|
||||
.education-cleanup-rules li { counter-increment: cleanup-rule; display: grid; grid-template-columns: 26px minmax(0, 1fr); gap: 4px 9px; }
|
||||
.education-cleanup-rules li::before { content: counter(cleanup-rule, decimal-leading-zero); grid-row: 1 / 3; color: var(--accent-light); font-size: 10px; font-weight: 800; }
|
||||
.education-cleanup-rules li b { color: var(--content-secondary); font-size: 11.5px; font-weight: 750; }
|
||||
.education-cleanup-rules li span { color: var(--content-muted); font-size: 10px; font-weight: 600; line-height: 1.55; }
|
||||
.education-cleanup-preview { margin-top: 13px; overflow: hidden; border: 1px solid color-mix(in srgb, var(--panel-edge) 68%, transparent); border-radius: 13px; }
|
||||
.education-cleanup-preview > header { padding: 13px 15px; background: color-mix(in srgb, var(--primitive-glass) 54%, transparent); }
|
||||
.education-cleanup-preview > header div { display: flex; align-items: baseline; justify-content: space-between; gap: 18px; }
|
||||
.education-cleanup-preview > header span { color: var(--content-muted); font-size: 9.5px; font-weight: 650; }
|
||||
.education-cleanup-preview > div { overflow: hidden; }
|
||||
.education-cleanup-change-list > header, .education-cleanup-change-list > article { display: grid; grid-template-columns: minmax(150px, .9fr) minmax(125px, .7fr) minmax(170px, 1.2fr) minmax(170px, 1.2fr); }
|
||||
.education-cleanup-change-list > header { background: color-mix(in srgb, var(--primitive-glass) 38%, transparent); }
|
||||
.education-cleanup-change-list > header span, .education-cleanup-change-list > article > span, .education-cleanup-change-list > article > b { min-width: 0; overflow: hidden; padding: 10px 12px; border-right: 1px solid color-mix(in srgb, var(--panel-edge) 52%, transparent); border-top: 1px solid color-mix(in srgb, var(--panel-edge) 52%, transparent); color: var(--content-secondary); text-overflow: ellipsis; white-space: nowrap; font-size: 10.5px; font-weight: 600; }
|
||||
.education-cleanup-change-list > header span { color: var(--content-primary); font-weight: 750; }
|
||||
.education-cleanup-change-list > article > b { color: var(--accent-light); font-weight: 700; }
|
||||
.education-cleanup-preview p { margin: 0; padding: 32px; color: var(--content-muted); text-align: center; font-size: 11.5px; }
|
||||
.education-automation-library { grid-template-rows: auto auto minmax(100px, 1fr) auto auto; }
|
||||
.education-new-rule { margin: 0 12px 12px; padding: 9px 11px; border: 1px solid color-mix(in srgb, var(--accent-light) 32%, transparent); border-radius: 9px; color: var(--content-secondary); background: color-mix(in srgb, var(--primitive-cool-glow) 7%, transparent); font-size: 11px; font-weight: 700; cursor: pointer; }
|
||||
.education-new-rule:disabled { opacity: .42; cursor: default; }
|
||||
.education-run-receipts { max-height: 190px; overflow: auto; padding: 12px 14px; border-top: 1px solid color-mix(in srgb, var(--panel-edge) 58%, transparent); }
|
||||
.education-run-receipts h3 { margin: 0 0 9px; color: var(--content-primary); font-size: 11px; font-weight: 750; }
|
||||
.education-run-receipts article { display: grid; grid-template-columns: auto 1fr; gap: 3px 8px; padding: 8px 0; border-top: 1px solid color-mix(in srgb, var(--panel-edge) 38%, transparent); }
|
||||
.education-run-receipts article b { color: var(--accent-light); font-size: 9.5px; font-weight: 750; }
|
||||
.education-run-receipts article span { color: var(--content-secondary); font-size: 9.5px; font-weight: 650; }
|
||||
.education-run-receipts article small { grid-column: 1 / -1; color: var(--content-muted); font-size: 8.5px; font-weight: 600; }
|
||||
.education-run-receipts p { margin: 0; color: var(--content-muted); font-size: 10px; }
|
||||
.education-automation-workbench { overflow: auto; padding: 20px; }
|
||||
.education-automation-workbench > header { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 2px 2px 18px; }
|
||||
.education-automation-workbench > header > div:first-child { display: grid; gap: 5px; }
|
||||
.education-automation-workbench > header span { color: var(--content-muted); font-size: 9.5px; font-weight: 750; letter-spacing: .1em; }
|
||||
.education-automation-workbench > header h2 { margin: 0; color: var(--content-primary); font-size: 22px; font-weight: 760; }
|
||||
.education-automation-workbench > header > div:last-child { display: flex; align-items: center; gap: 10px; }
|
||||
.education-automation-workbench > header label { display: flex; align-items: center; gap: 7px; color: var(--content-secondary); font-size: 10.5px; font-weight: 700; }
|
||||
.education-automation-workbench button { padding: 8px 12px; border: 1px solid color-mix(in srgb, var(--panel-edge) 72%, transparent); border-radius: 9px; color: var(--content-secondary); background: color-mix(in srgb, var(--primitive-glass) 58%, transparent); font-size: 10.5px; font-weight: 700; cursor: pointer; }
|
||||
.education-automation-workbench button:disabled { opacity: .42; cursor: default; }
|
||||
.education-automation-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; padding: 17px; border: 1px solid color-mix(in srgb, var(--panel-edge) 68%, transparent); border-radius: 13px; background: color-mix(in srgb, var(--primitive-glass) 42%, transparent); }
|
||||
.education-automation-form .wide { grid-column: 1 / -1; }
|
||||
.education-automation-form label { display: grid; gap: 6px; min-width: 0; }
|
||||
.education-automation-form label span { color: var(--content-muted); font-size: 9px; font-weight: 700; letter-spacing: .08em; }
|
||||
.education-automation-form input, .education-automation-form select { min-width: 0; width: 100%; height: 38px; padding: 0 11px; border: 1px solid color-mix(in srgb, var(--panel-edge) 70%, transparent); border-radius: 9px; outline: 0; color: var(--content-primary); background: color-mix(in srgb, var(--surface-depth) 82%, transparent); font-size: 11.5px; font-weight: 650; }
|
||||
.education-automation-form input:focus, .education-automation-form select:focus { border-color: color-mix(in srgb, var(--accent-light) 60%, transparent); }
|
||||
.education-automation-sentence { display: flex; align-items: end; gap: 10px; padding: 12px; border-radius: 11px; background: color-mix(in srgb, var(--primitive-cool-glow) 6%, transparent); }
|
||||
.education-automation-sentence > b { align-self: center; display: grid; place-items: center; flex: 0 0 34px; height: 34px; border-radius: 50%; color: var(--accent-light); background: color-mix(in srgb, var(--primitive-warm-glow) 13%, transparent); font-size: 13px; }
|
||||
.education-automation-sentence label { flex: 1 1 180px; }
|
||||
.education-automation-sentence label.grow { flex-grow: 2; }
|
||||
.education-automation-boundary, .education-automation-preview { margin-top: 13px; padding: 15px 17px; border: 1px solid color-mix(in srgb, var(--panel-edge) 68%, transparent); border-radius: 13px; background: color-mix(in srgb, var(--primitive-glass) 42%, transparent); }
|
||||
.education-automation-boundary h3, .education-automation-preview h3 { margin: 0; color: var(--content-primary); font-size: 14px; font-weight: 750; }
|
||||
.education-automation-boundary p, .education-automation-preview > p { margin: 8px 0 0; color: var(--content-muted); font-size: 10.5px; font-weight: 600; line-height: 1.65; }
|
||||
.education-automation-preview > header { display: flex; align-items: center; justify-content: space-between; gap: 18px; }
|
||||
.education-automation-preview > header > div { display: grid; gap: 4px; }
|
||||
.education-automation-preview > header span { color: var(--content-muted); font-size: 9px; font-weight: 700; letter-spacing: .09em; }
|
||||
.education-automation-metrics { display: grid; grid-template-columns: repeat(3, minmax(100px, 1fr)) minmax(170px, auto); gap: 10px; margin-top: 13px; }
|
||||
.education-automation-metrics article { display: grid; grid-template-columns: 1fr auto; align-items: end; gap: 3px 8px; padding: 12px; border-radius: 10px; background: color-mix(in srgb, var(--primitive-cool-glow) 7%, transparent); }
|
||||
.education-automation-metrics article span { grid-column: 1 / -1; color: var(--content-muted); font-size: 9px; font-weight: 700; }
|
||||
.education-automation-metrics article strong { color: var(--content-primary); font-size: 23px; font-weight: 760; }
|
||||
.education-automation-metrics article small { padding-bottom: 3px; color: var(--content-muted); font-size: 9px; font-weight: 650; }
|
||||
.education-automation-metrics .education-run-action { align-self: stretch; border-color: color-mix(in srgb, var(--accent-light) 42%, transparent); color: var(--content-primary); background: color-mix(in srgb, var(--primitive-warm-glow) 12%, transparent); }
|
||||
.composition-workspace-world { background: radial-gradient(circle at 70% 18%, color-mix(in srgb, var(--primitive-cool-glow) 14%, transparent), transparent 34%), radial-gradient(circle at 30% 80%, color-mix(in srgb, var(--primitive-warm-glow) 7%, transparent), transparent 34%), color-mix(in srgb, var(--surface-depth) 98%, transparent); }
|
||||
.native-composition-layout { min-width: 0; min-height: 0; display: grid; grid-template-columns: 294px minmax(0, 1fr); gap: 14px; padding: 14px; }
|
||||
.composition-recipe-panel, .composition-projection-stage { min-width: 0; min-height: 0; border: 1px solid color-mix(in srgb, var(--panel-edge) 78%, transparent); border-radius: 17px; background: color-mix(in srgb, var(--panel-bg) 76%, transparent); box-shadow: 0 26px 74px rgba(0, 0, 0, .24); overflow: hidden; }
|
||||
.composition-recipe-panel { overflow: auto; padding: 20px 18px; }
|
||||
.composition-recipe-panel > header { display: grid; gap: 5px; padding-bottom: 17px; border-bottom: 1px solid color-mix(in srgb, var(--panel-edge) 55%, transparent); }
|
||||
.composition-recipe-panel > header span, .composition-projection-stage > header span { color: var(--accent-light); font-size: 9.5px; font-weight: 800; letter-spacing: .16em; }
|
||||
.composition-recipe-panel > header h2 { margin: 0; color: var(--content-primary); font-size: 21px; font-weight: 760; }
|
||||
.composition-recipe-panel > header p { margin: 0; color: var(--content-muted); font-size: 10.5px; font-weight: 620; }
|
||||
.composition-source { display: flex; align-items: center; gap: 12px; margin: 17px 0 0; padding: 13px; border: 1px solid color-mix(in srgb, var(--accent-light) 23%, transparent); border-radius: 12px; background: color-mix(in srgb, var(--primitive-cool-glow) 8%, transparent); }
|
||||
.composition-source > i { flex: 0 0 33px; width: 33px; height: 33px; border-radius: 50%; background: radial-gradient(circle at 40% 35%, #fffce8, var(--accent-light) 24%, color-mix(in srgb, var(--primitive-cool-glow) 55%, transparent) 58%, transparent 70%); box-shadow: 0 0 22px color-mix(in srgb, var(--primitive-cool-glow) 38%, transparent); }
|
||||
.composition-source > div { min-width: 0; display: grid; gap: 3px; }
|
||||
.composition-source span, .composition-recipe-panel > label > span, .composition-recipe-panel legend { color: var(--content-muted); font-size: 9px; font-weight: 750; letter-spacing: .08em; }
|
||||
.composition-source b { overflow: hidden; color: var(--content-primary); font-size: 12px; font-weight: 730; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.composition-source small { color: var(--content-muted); font-size: 9.5px; font-weight: 600; }
|
||||
.composition-connector { width: 1px; height: 20px; margin: 0 auto; background: linear-gradient(transparent, color-mix(in srgb, var(--accent-light) 45%, transparent)); }
|
||||
.composition-recipe-panel > label { display: grid; gap: 6px; margin-bottom: 11px; }
|
||||
.composition-recipe-panel select { width: 100%; height: 38px; padding: 0 11px; border: 1px solid color-mix(in srgb, var(--panel-edge) 72%, transparent); border-radius: 9px; outline: 0; color: var(--content-primary); background: color-mix(in srgb, var(--surface-depth) 84%, transparent); font-size: 11.5px; font-weight: 650; }
|
||||
.composition-recipe-panel fieldset { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px; margin: 16px 0 0; padding: 12px; border: 1px solid color-mix(in srgb, var(--panel-edge) 64%, transparent); border-radius: 11px; }
|
||||
.composition-recipe-panel legend { padding: 0 6px; }
|
||||
.composition-recipe-panel fieldset label { display: flex; align-items: center; gap: 6px; color: var(--content-secondary); font-size: 10.5px; font-weight: 650; }
|
||||
.composition-recipe-panel input { accent-color: var(--accent-light); }
|
||||
.composition-execute { width: 100%; margin-top: 14px; padding: 11px 12px; border: 1px solid color-mix(in srgb, var(--accent-light) 40%, transparent); border-radius: 10px; color: var(--content-primary); background: linear-gradient(135deg, color-mix(in srgb, var(--primitive-warm-glow) 13%, transparent), color-mix(in srgb, var(--primitive-cool-glow) 9%, transparent)); font-size: 11.5px; font-weight: 750; cursor: pointer; }
|
||||
.composition-execute:disabled { opacity: .5; cursor: default; }
|
||||
.composition-recipe-panel > footer { display: grid; gap: 4px; margin-top: 15px; padding-top: 14px; border-top: 1px solid color-mix(in srgb, var(--panel-edge) 55%, transparent); color: var(--content-muted); font-size: 9.5px; font-weight: 600; }
|
||||
.composition-recipe-panel > footer b { color: var(--accent-light); font-size: 10px; font-weight: 750; }
|
||||
.composition-projection-stage { overflow: auto; padding: 20px; }
|
||||
.composition-projection-stage > header { display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 2px 2px 18px; }
|
||||
.composition-projection-stage > header > div:first-child { display: grid; gap: 5px; }
|
||||
.composition-projection-stage > header h1 { margin: 0; color: var(--content-primary); font-size: 25px; font-weight: 770; letter-spacing: .02em; }
|
||||
.composition-projection-stage > header p { margin: 0; color: var(--content-muted); font-size: 11px; font-weight: 620; }
|
||||
.composition-state { display: grid; grid-template-columns: 9px auto; align-items: center; gap: 2px 7px; padding: 9px 12px; border: 1px solid color-mix(in srgb, var(--accent-light) 24%, transparent); border-radius: 10px; background: color-mix(in srgb, var(--primitive-cool-glow) 7%, transparent); }
|
||||
.composition-state i { grid-row: 1 / 3; width: 7px; height: 7px; border-radius: 50%; background: var(--accent-light); box-shadow: 0 0 10px var(--accent-light); }
|
||||
.composition-state span { color: var(--content-secondary); font-size: 10px; font-weight: 700; }
|
||||
.composition-state small { color: var(--content-muted); font-size: 8.5px; font-weight: 600; }
|
||||
.composition-message { margin: -8px 2px 13px; color: var(--content-muted); font-size: 10px; font-weight: 620; }
|
||||
.composition-view-grid { display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(260px, .85fr); gap: 12px; align-items: start; }
|
||||
.composition-view { min-width: 0; overflow: hidden; border: 1px solid color-mix(in srgb, var(--panel-edge) 66%, transparent); border-radius: 14px; background: color-mix(in srgb, var(--primitive-glass) 46%, transparent); }
|
||||
.composition-view > header { display: flex; align-items: baseline; justify-content: space-between; gap: 15px; padding: 13px 15px; border-bottom: 1px solid color-mix(in srgb, var(--panel-edge) 50%, transparent); }
|
||||
.composition-view > header span { color: var(--content-primary); font-size: 13px; font-weight: 750; }
|
||||
.composition-view > header small { color: var(--content-muted); font-size: 9px; font-weight: 620; }
|
||||
.composition-dashboard, .composition-table { grid-column: 1 / -1; }
|
||||
.composition-dashboard > div { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1px; }
|
||||
.composition-dashboard article { display: grid; grid-template-columns: 1fr auto; align-items: end; gap: 4px 8px; padding: 15px; background: color-mix(in srgb, var(--panel-bg) 32%, transparent); }
|
||||
.composition-dashboard article span { grid-column: 1 / -1; color: var(--content-muted); font-size: 9px; font-weight: 700; }
|
||||
.composition-dashboard article strong { color: var(--content-primary); font-size: 24px; font-weight: 760; }
|
||||
.composition-dashboard article small { padding-bottom: 3px; color: var(--content-muted); font-size: 9px; font-weight: 650; }
|
||||
.composition-bar-plot { height: 222px; display: flex; align-items: stretch; gap: 8px; padding: 18px 16px 12px; }
|
||||
.composition-bar-plot article { min-width: 0; flex: 1 1 44px; display: grid; grid-template-rows: minmax(0, 1fr) 32px; gap: 7px; }
|
||||
.composition-bar-track { position: relative; display: flex; align-items: end; justify-content: center; border-bottom: 1px solid color-mix(in srgb, var(--panel-edge) 70%, transparent); }
|
||||
.composition-bar-track > i { width: min(38px, 72%); height: var(--bar-height); min-height: 4px; border-radius: 7px 7px 2px 2px; background: linear-gradient(to top, color-mix(in srgb, var(--primitive-cool-glow) 50%, transparent), color-mix(in srgb, var(--accent-light) 76%, #fff)); box-shadow: 0 -4px 18px color-mix(in srgb, var(--primitive-cool-glow) 24%, transparent); }
|
||||
.composition-bar-track > em { position: absolute; top: -4px; color: var(--content-muted); font-size: 8px; font-style: normal; font-weight: 650; white-space: nowrap; }
|
||||
.composition-bar-plot article > b { overflow: hidden; color: var(--content-secondary); text-align: center; text-overflow: ellipsis; white-space: nowrap; font-size: 9px; font-weight: 650; }
|
||||
.composition-comparison > div { display: grid; gap: 10px; padding: 14px; }
|
||||
.composition-comparison article { display: grid; grid-template-columns: 1fr auto; gap: 5px 9px; }
|
||||
.composition-comparison article > div { grid-column: 1 / -1; display: flex; justify-content: space-between; gap: 12px; }
|
||||
.composition-comparison b { color: var(--content-secondary); font-size: 10px; font-weight: 700; }
|
||||
.composition-comparison span, .composition-comparison small { color: var(--content-muted); font-size: 9px; font-weight: 620; }
|
||||
.composition-comparison article > i { overflow: hidden; height: 6px; border-radius: 5px; background: color-mix(in srgb, var(--panel-edge) 56%, transparent); }
|
||||
.composition-comparison article > i > em { display: block; width: var(--share); height: 100%; border-radius: inherit; background: linear-gradient(90deg, color-mix(in srgb, var(--primitive-cool-glow) 48%, transparent), var(--accent-light)); }
|
||||
.composition-classification > div { display: grid; max-height: 270px; overflow: auto; }
|
||||
.composition-classification article { display: grid; grid-template-columns: 24px minmax(0, 1fr) auto; align-items: center; gap: 9px; padding: 11px 14px; border-top: 1px solid color-mix(in srgb, var(--panel-edge) 42%, transparent); }
|
||||
.composition-classification article > i { color: var(--accent-light); font-size: 9px; font-style: normal; font-weight: 800; }
|
||||
.composition-classification article > div { min-width: 0; display: grid; gap: 3px; }
|
||||
.composition-classification b { overflow: hidden; color: var(--content-primary); font-size: 10.5px; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.composition-classification span { color: var(--content-muted); font-size: 8.5px; font-weight: 600; }
|
||||
.composition-classification strong { color: var(--content-secondary); font-size: 10px; font-weight: 700; }
|
||||
.composition-table > div { max-height: 320px; overflow: auto; }
|
||||
.composition-table table { min-width: 100%; border-collapse: collapse; }
|
||||
.composition-table th, .composition-table td { max-width: 260px; padding: 10px 12px; border-top: 1px solid color-mix(in srgb, var(--panel-edge) 45%, transparent); color: var(--content-secondary); text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 9.5px; font-weight: 600; }
|
||||
.composition-table th { position: sticky; z-index: 2; top: 0; color: var(--content-primary); background: color-mix(in srgb, var(--panel-bg) 96%, transparent); font-weight: 750; }
|
||||
.composition-empty { display: grid; justify-items: center; width: min(520px, calc(100% - 40px)); margin: 15vh auto 0; text-align: center; }
|
||||
.composition-empty > span { display: grid; place-items: center; width: 66px; height: 66px; border-radius: 50%; color: var(--accent-light); background: radial-gradient(circle, color-mix(in srgb, var(--primitive-cool-glow) 22%, transparent), transparent 72%); font-size: 20px; font-weight: 760; }
|
||||
.composition-empty h2 { margin: 16px 0 7px; color: var(--content-primary); font-size: 21px; }
|
||||
.composition-empty p { margin: 0; color: var(--content-muted); font-size: 11.5px; line-height: 1.7; }
|
||||
@media (max-width: 980px) {
|
||||
.native-composition-layout { grid-template-columns: 250px minmax(0, 1fr); }
|
||||
.composition-view-grid { grid-template-columns: 1fr; }
|
||||
.composition-view { grid-column: 1; }
|
||||
.composition-dashboard > div { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
.private-route-note { position: absolute; z-index: 10; left: 50%; bottom: 11%; width: min(720px, calc(100% - 64px)); margin: 0; transform: translateX(-50%); color: var(--content-muted); text-align: center; font-size: 12.5px; font-weight: 650; letter-spacing: .08em; }
|
||||
.personal-node-guide { position: absolute; z-index: 12; left: 50%; top: 55%; width: min(760px, calc(100% - 72px)); max-height: calc(100% - 190px); overflow: auto; padding: 26px 30px; transform: translate(-50%, -50%); border: 1px solid var(--panel-edge); border-radius: 20px; color: var(--content-secondary); background: color-mix(in srgb, var(--panel-bg) 88%, transparent); box-shadow: 0 30px 90px rgba(0, 0, 0, .42); backdrop-filter: blur(22px); }
|
||||
.personal-node-guide header span { color: var(--accent-light); font-size: 11px; font-weight: 750; letter-spacing: .2em; }
|
||||
|
|
@ -763,6 +1226,7 @@ svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-lineca
|
|||
.time-module-strip em { color: var(--content-faint); font-size: 10px; font-style: normal; white-space: nowrap; }
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.world-title-actions > span { display: none; }
|
||||
.era-home-entry { left: 14px; bottom: 10px; width: 282px; }
|
||||
.era-timeline-panel li { grid-template-columns: 104px 18px minmax(0, 1fr); gap: 10px; }
|
||||
.era-timeline-panel ol::before { left: 142px; }
|
||||
|
|
@ -772,11 +1236,21 @@ svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-lineca
|
|||
.receipt-message { color: var(--accent-light) !important; font-weight: 650; text-align: right; }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.education-focus-hero { grid-template-columns: 1fr; }
|
||||
.education-focus-hero > article { justify-items: start; }
|
||||
.education-focus-hero > article > div { justify-content: flex-start; }
|
||||
.education-dashboard-metrics { grid-template-columns: repeat(2, minmax(130px, 1fr)); }
|
||||
.education-dashboard-chart article { grid-template-columns: minmax(80px, 120px) minmax(120px, 1fr) 30px; }
|
||||
.education-dashboard-chart article em { grid-column: 2 / 4; }
|
||||
.d-main { left: 2%; } .d-sub { left: 22%; } .d-zero { right: 22%; } .d-zs { right: 2%; }
|
||||
.domain-info-card.info-main { left: 18px; } .domain-info-card.info-sub { left: calc(22% - 75px); }
|
||||
.domain-info-card.info-zero { right: calc(22% - 75px); } .domain-info-card.info-zs { right: 18px; }
|
||||
.world-pool { width: 150px; } .pool-bay { width: 145px; }
|
||||
.channel-knowledge { left: 4%; } .channel-code { left: 25%; } .channel-light { right: 25%; } .channel-status { right: 4%; }
|
||||
.industry-pool { width: 180px; }
|
||||
.industry-pool .pool-bay { width: 170px; }
|
||||
.industry-web-novel { left: 4%; } .industry-pet { right: 4%; }
|
||||
.education-module-1 { left: 3%; } .education-module-2 { left: 23%; } .education-module-3 { right: 23%; } .education-module-4 { right: 3%; }
|
||||
.broadcast-stream { left: 5%; }
|
||||
.personal-node-guide ol { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
"enterprise_relationship_and_responsibility_client_commands": "SOURCE_INTEGRATED_QODER_UI_BOUND_MACOS_KEYCHAIN_SIGNING_READY",
|
||||
"qoder_official_ui_template": "REPO_012_27D34DF_OFFICIAL_LOGIN_V0_4_AND_INNER_SCREENS_V0_1_RUNTIME_INTEGRATED_BROWSER_VERIFIED",
|
||||
"client_world_navigation": "DOMAIN_HOME_TO_CHANNEL_OVERVIEW_TO_TOOL_PROJECTION_NO_TRADITIONAL_GLOBAL_SIDEBAR",
|
||||
"authenticated_main_world_navigation": "SOURCE_INTEGRATED_SESSION_PRESERVING_REENTRY",
|
||||
"prelogin_private_broker": "WAITING_FOR_LOGIN_NO_PRIVATE_STORAGE_OPENED",
|
||||
"mac_arm64_desktop_install_acceptance": "PASS_DESKTOP_HOLOLAKE_APP_REAL_PROCESS_AND_FIVE_DOMAIN_UI_READBACK",
|
||||
"mac_x86_64_installer": "NOT_BUILT",
|
||||
|
|
@ -44,7 +45,23 @@
|
|||
"generic_ai_connection": "EXPRESSION_ONLY_VISITOR",
|
||||
"guanghu_persona_connection": "SEPARATE_BINDING_EVIDENCE_REQUIRED_NOT_YET_IMPLEMENTED",
|
||||
"user_native_code_channel": "HOLOLAKE_SHELL_OVER_APP_OWNED_GIT_AFTER_DOMAIN_ROUTE_AND_LOGIN",
|
||||
"forgejo_role": "OPTIONAL_REMOTE_COLLABORATION_ADAPTER_NOT_PRODUCT_SHELL"
|
||||
"forgejo_role": "OPTIONAL_REMOTE_COLLABORATION_ADAPTER_NOT_PRODUCT_SHELL",
|
||||
"subdomain_industry_registry": "SOURCE_INTEGRATED_AND_DESKTOP_UI_ACCEPTED",
|
||||
"subdomain_industry_states": "WEB_NOVEL_PLANNED_PET_PLANNED_EDUCATION_IN_DEVELOPMENT",
|
||||
"industry_entry_domain": "BRANCH_DOMAIN",
|
||||
"zero_domain_industry_entry": "ABSENT_BY_CURRENT_CORRECTION",
|
||||
"education_initialization_channel": "BUILT_IN_CHANNEL_WORKBENCH_WITH_DEVELOPMENT_EDUCATION_MODULE",
|
||||
"education_workspace_storage": "AUTHENTICATED_ACCOUNT_SCOPED_SQLITE",
|
||||
"education_workspace_restart_readback": "RUST_TESTED",
|
||||
"education_workspace_second_slice": "DOCUMENT_TEMPLATES_AND_DIRECTORY_FILTER_PLUS_TABLE_FILTER_SORT_GRID_CARD_VIEWS_AND_BOUNDED_TABULAR_PASTE",
|
||||
"education_workspace_data_cleanup": "PREVIEW_TRIM_CELL_BOUNDARIES_REMOVE_FULLY_EMPTY_AND_EXACT_DUPLICATE_ROWS_HUMAN_CONFIRMED_REVISION_SAVE",
|
||||
"education_workspace_manual_automation": "SAVED_RULES_EQUALS_CONTAINS_EMPTY_NONEMPTY_PREVIEW_HUMAN_CONFIRM_REVISION_LOCK_ATOMIC_EXECUTION_RECEIPT_NO_BACKGROUND_SCHEDULE",
|
||||
"education_workspace_native_composition": "REAL_ACCOUNT_KNOWLEDGE_NATIVE_OBJECT_TYPED_DAG_AND_FIVE_READ_ONLY_HUMAN_PROJECTIONS_SOURCE_IMPLEMENTED",
|
||||
"education_workspace_open_source_donor_review": "APPFLOWY_LOGIC_ONLY_VEGA_LITE_OBSERVABLE_PLOT_ARROW_DATAFUSION_ECHARTS_DUCKDB_AND_TANSTACK_REVIEWED_NO_NEW_RUNTIME_IMPORTED",
|
||||
"education_workspace_desktop_ui_acceptance": "PASS_MAIN_WORLD_TO_BRANCH_DOMAIN_TO_EDUCATION_TO_FIVE_NATIVE_MODULES_AND_REAL_129_ROW_FIVE_VIEW_COMPOSITION_WITH_RECOMPOSITION_RECEIPTS",
|
||||
"education_workspace_desktop_binary_sha256": "2a26acae4a706c0ff9919b85f77826a6ca71b650ec22209befd30cd1456c92f3",
|
||||
"education_workspace_public_release": "NOT_PUBLISHED",
|
||||
"human_and_industry_number_boundary": "SEPARATE_HUMAN_IDENTITY_AND_INDUSTRY_ROUTING_NAMESPACES"
|
||||
},
|
||||
"read_order": [
|
||||
"product-source/hololake-platform/architecture/HOLOLAKE-TCS-WRITTEN-WORK-OWNERSHIP-AND-REALITY-ENGINEERING-GATE-20260812.md",
|
||||
|
|
|
|||
Loading…
Reference in a new issue