chore: snapshot audited HoloLake convergence baseline

This commit is contained in:
冰朔 2026-08-18 17:58:07 +08:00
commit 1b6b17b5ab
47 changed files with 10722 additions and 87 deletions

View file

@ -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`。本机身份不是平台账户
认证;本机知识与源码访问也不产生仓库、服务器、部署或发布权限。

View file

@ -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"
}
}

View file

@ -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
}
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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
}
}

View file

@ -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"
}
}

View file

@ -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"
]
}
]
}

View file

@ -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
}
}

View file

@ -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"],

View file

@ -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"
]
}

View file

@ -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`

View file

@ -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",

View file

@ -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"
}
}
}
}

View file

@ -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"
}
}

View file

@ -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',
)
})

View file

@ -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, /系统不会在后台自行运行/)
})

View file

@ -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/)
})

View file

@ -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/)
})

View file

@ -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)
})

View file

@ -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',
)
})

View file

@ -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",
]

View file

@ -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"

View file

@ -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

View file

@ -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());
}
}

View file

@ -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,

View file

@ -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, &registry())?;
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, &registry()).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, &registry()).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"
);
}
}

View file

@ -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");
}
}

View file

@ -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()

View file

@ -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

View file

@ -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>
}

View file

@ -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

View file

@ -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>
}

View file

@ -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(['内容'])
})
})

View file

@ -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 }
}

View file

@ -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: ['模型建议已通过本地结构校验、资源上限与用户确认。'],
}
}

View file

@ -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)
})
})

View file

@ -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 }
}

View file

@ -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="从这里开始记录教学计划、会议纪要或课程资料……"/>
}

View file

@ -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>
}

View file

@ -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

View file

@ -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'])
})
})

View file

@ -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; }
}

View file

@ -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",