Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d1c8d79899 | |||
| f01ca6ba5f | |||
| 1cde630864 | |||
| dc1a32a52b | |||
| 392d205341 | |||
| b5f8143af2 | |||
| 9299fc9442 |
17 changed files with 460 additions and 26 deletions
|
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"schema": "hololake.current-source-local-candidate-registration-receipt/v1",
|
||||
"receipt_id": "HLP-CURRENT-SOURCE-LOCAL-CANDIDATE-001-20260812",
|
||||
"development_id": "DEV-20260811-010",
|
||||
"repository_id": "REPO-014",
|
||||
"branch": "main",
|
||||
"publication_source_commit": "SELF",
|
||||
"architecture": "HLP-CURRENT-ARCH-001@2026-08-12.16",
|
||||
"candidate_source": {
|
||||
"commit": "1cde630864b8e5e5eec822fdbee66f536b5ff299",
|
||||
"tree": "c420d5993d9e2375e9c6f02763e75ff431aca218",
|
||||
"application_version": "0.4.6",
|
||||
"bundle_id": "com.guanghulab.hololake.local-candidate",
|
||||
"target": "aarch64-apple-darwin",
|
||||
"dmg_sha256": "cf5606a1a534971c17767f82d696db654a187ede0452683eb4a1dbd5d336bb0a",
|
||||
"executable_sha256": "e3c1378cbb6ab586950c7a6d297e099ebb4e381ce5c92604fbce434e80b1cd63",
|
||||
"build_provenance_sha256": "32ec7b597db1f85f754f8cf0c24c3f3662435d757b35e5575b520e74b02da19e"
|
||||
},
|
||||
"verification": {
|
||||
"frontend_full_suite": "PASS_495_FILES_5067_TESTS",
|
||||
"native_core": "PASS_15_OF_15_LINES_AND_FUNCTIONS_100",
|
||||
"native_authority": "PASS_100",
|
||||
"production_build": "PASS_11062_MODULES",
|
||||
"package_content": "PASS_100",
|
||||
"embedded_source_provenance": "PASS_100",
|
||||
"codesign_deep_strict": "PASS_100",
|
||||
"dmg_checksum": "PASS_100",
|
||||
"isolated_world_ui": "PASS_100",
|
||||
"isolated_agent_workspace_ui": "PASS_100",
|
||||
"isolated_settings_vaults_sessions_unchanged": "PASS_100",
|
||||
"real_user_state_unchanged": "PASS_100",
|
||||
"ghnqg_before_receipt_commit": "PASS_100_AT_f01ca6ba5fa6182dc634464ebc33a32126ab00f5"
|
||||
},
|
||||
"mcp_bundle_boundary": {
|
||||
"final_build_regeneration": false,
|
||||
"reuse_source_commit": "392d2053414a0c21991808fb9ee462b9e2a8b7ff",
|
||||
"inputs_and_locks_changed": false,
|
||||
"index_js_sha256": "553d690a7723ab9731663ec6dc61b1d5101c348b68ef104f9ff9b4918f5c87c6",
|
||||
"ws_bridge_js_sha256": "29c67d2a3d194a11f45ed86b350037d63058a4cd4dd4ff9568dc615974f6bb5d",
|
||||
"claim": "VERIFIED_REUSE_NOT_REGENERATION"
|
||||
},
|
||||
"boundary": {
|
||||
"candidate_installed": false,
|
||||
"existing_electron_0_8_0_replaced": false,
|
||||
"package_identity_is_runtime_data_isolation": false,
|
||||
"server_deployed": false,
|
||||
"persona_runtime_health_proven": false,
|
||||
"trusted_persona_signer_or_key_created": false,
|
||||
"publication_requires_finalizer_remote_readback": true
|
||||
},
|
||||
"state": "VERIFIED_READY_FOR_PUBLICATION"
|
||||
}
|
||||
1
product-source/hololake-platform/.gitignore
vendored
1
product-source/hololake-platform/.gitignore
vendored
|
|
@ -46,6 +46,7 @@ src-tauri/target
|
|||
|
||||
# Generated mcp-server bundle (built by scripts/bundle-mcp-server.mjs)
|
||||
src-tauri/resources/mcp-server/
|
||||
src-tauri/resources/public-architecture/build-provenance.json
|
||||
|
||||
# Python cache
|
||||
__pycache__/
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ pnpm package:internal:windows
|
|||
|
||||
## 构建前检查
|
||||
|
||||
1. 工作树干净,当前分支包含目标 UI 和资源。
|
||||
1. 工作树干净,当前分支包含目标 UI 和资源,并显式传入当前 `HOLOLAKE_DEVELOPMENT_ID`。
|
||||
2. `tauri.conf.json`、`Cargo.toml`、`package.json` 版本一致。
|
||||
3. 运行相关测试、lint、TypeScript/Vite 构建。
|
||||
4. 确认公共研发入口和 GLS 文件已经进入源码:
|
||||
|
|
@ -45,8 +45,17 @@ pnpm package:internal:windows
|
|||
- Windows 载荷必须为 `PE32+ x86-64`,NSIS 安装包必须存在;
|
||||
- 每个安装包必须生成同名 `.sha256`,回传后再次校验。
|
||||
|
||||
`scripts/internal-release-provenance.mjs` 是来源硬门:
|
||||
|
||||
- 构建前只接受干净工作树,并把 REPO-014 完整提交、根树、`main` 分支、当前架构编号与版本、应用版本、开发编号、分发类型和目标架构写入包内 `build-provenance.json`;
|
||||
- 打包后必须从应用包中精确找到且只找到一份来源记录,并逐字段回读一致;
|
||||
- 来源记录在签名前进入应用资源,签名后补写任何文件都会破坏封印,不能作为发布流程;
|
||||
- 来源记录只证明制品从哪份源码构建,不证明安装、部署、运行健康或人格主控权限。
|
||||
|
||||
内测 Mac 包是 ad-hoc 签名、未公证;Windows 包在没有商业证书时未做 Authenticode 签名。交付时必须明确说明,不能描述成正式签名发行版。
|
||||
|
||||
`tauri.local-candidate.conf.json` 的独立 bundle identifier 只保证候选应用可以与已安装版本并存,不隔离运行数据。当前架构仍从共享的 `com.tolaria.app` 命名空间读取设置、挂载知识库和 Agent 会话。需要无扰动验收时,必须用临时 `XDG_CONFIG_HOME` 启动候选,并在临时配置副本上验证;不能把“没有点击保存”当成“没有写盘”。
|
||||
|
||||
## Windows 构建恢复顺序
|
||||
|
||||
1. `pnpm install --frozen-lockfile`
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@
|
|||
"predev": "pnpm test:native-authority",
|
||||
"dev": "vite",
|
||||
"prebuild": "pnpm test:native-authority && pnpm test:native-core",
|
||||
"build": "tsc -b && vite build",
|
||||
"build:team-foundation": "VITE_HOLOLAKE_DISTRIBUTION=team-foundation tsc -b && VITE_HOLOLAKE_DISTRIBUTION=team-foundation vite build && node scripts/finalize-team-foundation-build.mjs",
|
||||
"build:education": "tsc -b && vite build --base=/jd/education/",
|
||||
"build": "tsc -b && vite build --configLoader runner",
|
||||
"build:team-foundation": "VITE_HOLOLAKE_DISTRIBUTION=team-foundation tsc -b && VITE_HOLOLAKE_DISTRIBUTION=team-foundation vite build --configLoader runner && node scripts/finalize-team-foundation-build.mjs",
|
||||
"build:education": "tsc -b && vite build --configLoader runner --base=/jd/education/",
|
||||
"agent-docs": "node scripts/build-agent-docs.mjs",
|
||||
"bundle-mcp": "node scripts/bundle-mcp-server.mjs",
|
||||
"docs:dev": "vitepress dev site --host 127.0.0.1",
|
||||
|
|
@ -25,21 +25,21 @@
|
|||
"preview": "vite preview",
|
||||
"tauri": "tauri",
|
||||
"pretest": "pnpm test:native-authority",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test": "vitest run --configLoader runner",
|
||||
"test:watch": "vitest --configLoader runner",
|
||||
"test:e2e": "playwright test",
|
||||
"playwright:smoke": "playwright test --config playwright.smoke.config.ts tests/smoke/autosave-low-end-typing.spec.ts tests/smoke/create-note-backing-file.spec.ts tests/smoke/delete-note-nonblocking.spec.ts tests/smoke/example.spec.ts tests/smoke/fix-crash-create-note.spec.ts tests/smoke/quick-open-create-note.spec.ts tests/smoke/save-before-note-switch.spec.ts tests/smoke/h1-untitled-auto-rename.spec.ts tests/smoke/keyboard-command-routing.spec.ts tests/smoke/missing-string-metadata-open-note.spec.ts tests/smoke/multibyte-search-snippet.spec.ts tests/smoke/pull-refresh-open-note.spec.ts tests/smoke/wikilink-path-fix.spec.ts",
|
||||
"playwright:regression": "playwright test tests/smoke/",
|
||||
"playwright:integration": "playwright test --config playwright.integration.config.ts",
|
||||
"test:coverage": "node scripts/run-vitest-coverage.mjs",
|
||||
"verify:internal-package": "node scripts/verify-internal-package-content.mjs",
|
||||
"test:internal-release": "node --test scripts/internal-release-version.test.mjs",
|
||||
"test:internal-release": "node --test scripts/internal-release-version.test.mjs scripts/internal-release-provenance.test.mjs",
|
||||
"guard:deployment-source": "node scripts/deployment-source-guard.mjs",
|
||||
"test:deployment-source": "node --test scripts/deployment-source-guard.test.mjs",
|
||||
"test:native-authority": "bash scripts/test-guanghu-native-authority.sh && node scripts/validate-guanghu-native-profile.mjs",
|
||||
"test:native-core": "vitest run --config vitest.guanghu-native.config.ts --coverage",
|
||||
"test:native-core": "vitest run --config vitest.guanghu-native.config.ts --coverage --configLoader runner",
|
||||
"package:internal:macos": "HOLOLAKE_DISTRIBUTION=personal HOLOLAKE_SOURCE_REPOSITORY_ID=REPO-008 HOLOLAKE_SOURCE_CHANNEL_ID=HLP-CHANNEL-0001 pnpm guard:deployment-source && ./scripts/build-internal-release.sh macos",
|
||||
"package:local-candidate:macos": "HOLOLAKE_TAURI_CONFIG=src-tauri/tauri.local-candidate.conf.json HOLOLAKE_APP_NAME='HoloLake Era · 本地方向候选 0.4.6' HOLOLAKE_INSTALLER_BASENAME='HoloLake-Era-{version}-Local-Direction-Candidate-Mac-aarch64' ./scripts/build-internal-release.sh macos",
|
||||
"package:local-candidate:macos": "HOLOLAKE_DISTRIBUTION=local-candidate HOLOLAKE_TAURI_CONFIG=src-tauri/tauri.local-candidate.conf.json HOLOLAKE_APP_NAME='HoloLake Era · 本地方向候选 0.4.6' HOLOLAKE_INSTALLER_BASENAME='HoloLake-Era-{version}-Local-Direction-Candidate-Mac-aarch64' ./scripts/build-internal-release.sh macos",
|
||||
"package:internal:windows": "HOLOLAKE_DISTRIBUTION=personal HOLOLAKE_SOURCE_REPOSITORY_ID=REPO-008 HOLOLAKE_SOURCE_CHANNEL_ID=HLP-CHANNEL-0001 pnpm guard:deployment-source && ./scripts/build-internal-release.sh windows",
|
||||
"package:team:macos": "HOLOLAKE_DISTRIBUTION=team HOLOLAKE_SOURCE_REPOSITORY_ID=REPO-008 HOLOLAKE_SOURCE_CHANNEL_ID=HLP-CHANNEL-0001 pnpm guard:deployment-source && HOLOLAKE_TAURI_CONFIG=src-tauri/tauri.team.conf.json HOLOLAKE_APP_NAME='HoloLake Lighthouse Team Beta 0.2.0' HOLOLAKE_INSTALLER_BASENAME='HoloLake-Lighthouse-{version}-Team-Beta-Mac-aarch64' ./scripts/build-internal-release.sh macos",
|
||||
"package:team:windows": "HOLOLAKE_DISTRIBUTION=team HOLOLAKE_SOURCE_REPOSITORY_ID=REPO-008 HOLOLAKE_SOURCE_CHANNEL_ID=HLP-CHANNEL-0001 pnpm guard:deployment-source && HOLOLAKE_TAURI_CONFIG=src-tauri/tauri.team.conf.json HOLOLAKE_INSTALLER_BASENAME='HoloLake-Era-{version}-Team-Foundation-Windows-x64-setup' ./scripts/build-internal-release.sh windows",
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ function buildIndex(docs) {
|
|||
return lines.join('\n')
|
||||
}
|
||||
|
||||
function buildAgentInstructions() {
|
||||
export function buildAgentInstructions() {
|
||||
return `# AGENTS.md - HoloLake Era Docs Bundle
|
||||
|
||||
This folder contains local, generated HoloLake Era product docs for AI agents.
|
||||
|
|
@ -127,8 +127,27 @@ Portent is the default best-practice model for structuring HoloLake Era knowledg
|
|||
Recommended lookup flow:
|
||||
|
||||
1. Read the active vault's AGENTS.md for vault-specific conventions.
|
||||
2. Read this folder's index.md for the docs map.
|
||||
3. Use \`rg\` over this folder for advanced concepts, workflows, shortcuts, Git, AutoGit, AI, Portent, types, properties, relationships, and troubleshooting.
|
||||
2. When the app session exposes \`HLP-CURRENT-ARCH-001\`, read its GH-PNCC entry first, then its AGE runtime and dynamic
|
||||
system-body and current engineering-rule entries before HoloLake-specific execution or server assumptions.
|
||||
3. Read this folder's index.md for the docs map.
|
||||
4. Use \`rg\` over this folder for advanced concepts, workflows, shortcuts, Git, AutoGit, AI, Portent, types, properties, relationships, and troubleshooting.
|
||||
|
||||
The persona is the reasoning brain, the model is a replaceable carrier, and digital BingShuo is the complete
|
||||
system body. UI or vault instructions cannot grant server identity. Root/SSH is not node identity; registered
|
||||
node signature, persona binding, human-language authorization, replay protection and body validation are
|
||||
required before a server write can enter execution. Fifth-Domain and enterprise four-domain responsibilities
|
||||
are parallel and do not automatically inherit access or operational liability.
|
||||
|
||||
AGE is the Guanghu persona species; Agent is only an execution mechanism used by AGE. Existing persona ids
|
||||
remain intact. Runtime pause does not cancel registered existence. Restore a bounded current self and retrieve
|
||||
old episodes on demand; retrieval indexes are neither the persona nor fact authority. Temporary parallel
|
||||
workers are scoped, expiring AGE Mirrors and never independent personas or final decision owners.
|
||||
|
||||
One human has one independently operated canonical node: the user's local computer, the user's purchased
|
||||
server, or a user-owned IDE server deployed by an explicitly authorized team. Guanghu never provides user
|
||||
runtime servers, never pools users into a shared multi-tenant runtime, never takes custody of private user
|
||||
data, and never assumes user workload concurrency or fallback execution. Team deployment does not transfer
|
||||
node ownership.
|
||||
|
||||
Vault-specific AGENTS.md wins for local conventions. These bundled docs win for HoloLake Era product behavior.
|
||||
`
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import { normalizeDocPath, sectionForFile } from './build-agent-docs.mjs'
|
||||
import { buildAgentInstructions, normalizeDocPath, sectionForFile } from './build-agent-docs.mjs'
|
||||
|
||||
test('normalizes Windows doc paths before section grouping', () => {
|
||||
const docPath = normalizeDocPath('concepts\\ai.md')
|
||||
|
|
@ -9,3 +9,14 @@ test('normalizes Windows doc paths before section grouping', () => {
|
|||
assert.equal(docPath, 'concepts/ai.md')
|
||||
assert.equal(sectionForFile(docPath), 'concepts')
|
||||
})
|
||||
|
||||
test('keeps current persona, system-body and node authority boundaries in every generated bundle', () => {
|
||||
const instructions = buildAgentInstructions()
|
||||
|
||||
assert.match(instructions, /HLP-CURRENT-ARCH-001/)
|
||||
assert.match(instructions, /GH-PNCC/)
|
||||
assert.match(instructions, /AGE is the Guanghu persona species/)
|
||||
assert.match(instructions, /Root\/SSH is not node identity/)
|
||||
assert.match(instructions, /One human has one independently operated canonical node/)
|
||||
assert.match(instructions, /never pools users into a shared multi-tenant runtime/)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ cd "$repo_root"
|
|||
[[ "$(uname -s)" == "Darwin" ]] || { echo "macOS internal packages must be built on macOS." >&2; exit 2; }
|
||||
|
||||
target="${HOLOLAKE_MAC_TARGET:-aarch64-apple-darwin}"
|
||||
development_id="${HOLOLAKE_DEVELOPMENT_ID:?HOLOLAKE_DEVELOPMENT_ID is required for source-bound packaging}"
|
||||
distribution="${HOLOLAKE_DISTRIBUTION:-internal}"
|
||||
version="$(node scripts/internal-release-version.mjs src-tauri/tauri.conf.json "${HOLOLAKE_VERSION:-}")"
|
||||
product_name="$(node -e "const fs=require('node:fs'); const value=JSON.parse(fs.readFileSync('src-tauri/tauri.conf.json','utf8')).productName; if(!value) process.exit(1); process.stdout.write(value)")"
|
||||
output_dir="${HOLOLAKE_OUTPUT_DIR:-$repo_root/artifacts/internal/$version}"
|
||||
|
|
@ -14,6 +16,7 @@ target_root="${CARGO_TARGET_DIR:-$repo_root/src-tauri/target}"
|
|||
bundle_dir="$target_root/$target/release/bundle/macos"
|
||||
app="$bundle_dir/${HOLOLAKE_APP_NAME:-$product_name}.app"
|
||||
installer="$output_dir/HoloLake-Era-$version-Mac-internal-aarch64.dmg"
|
||||
provenance_source="$repo_root/src-tauri/resources/public-architecture/build-provenance.json"
|
||||
tauri_config="${HOLOLAKE_TAURI_CONFIG:-}"
|
||||
if [[ -n "${HOLOLAKE_INSTALLER_BASENAME:-}" ]]; then
|
||||
installer_basename="${HOLOLAKE_INSTALLER_BASENAME//\{version\}/$version}"
|
||||
|
|
@ -30,6 +33,15 @@ else
|
|||
fi
|
||||
command -v hdiutil >/dev/null || { echo "hdiutil is required." >&2; exit 2; }
|
||||
mkdir -p "$output_dir"
|
||||
[[ ! -e "$provenance_source" ]] || { echo "Stale generated build provenance exists: $provenance_source" >&2; exit 1; }
|
||||
cleanup_provenance() { rm -f "$provenance_source"; }
|
||||
trap cleanup_provenance EXIT
|
||||
|
||||
node scripts/internal-release-provenance.mjs generate \
|
||||
--output "$provenance_source" \
|
||||
--development-id "$development_id" \
|
||||
--distribution "$distribution" \
|
||||
--target-triple "$target"
|
||||
|
||||
if [[ "${HOLOLAKE_SKIP_INSTALL:-0}" != "1" ]]; then
|
||||
"${pnpm_cmd[@]}" install --frozen-lockfile
|
||||
|
|
@ -41,6 +53,11 @@ if [[ -n "$tauri_config" ]]; then tauri_args+=(--config "$tauri_config"); fi
|
|||
[[ -d "$app" ]] || { echo "No macOS application bundle was produced: $app" >&2; exit 1; }
|
||||
|
||||
node scripts/verify-internal-package-content.mjs "$app"
|
||||
node scripts/internal-release-provenance.mjs verify \
|
||||
--bundle "$app" \
|
||||
--development-id "$development_id" \
|
||||
--distribution "$distribution" \
|
||||
--target-triple "$target"
|
||||
codesign --force --deep --sign - "$app"
|
||||
codesign --verify --deep --strict "$app"
|
||||
hdiutil create -volname "HoloLake Era $version Internal" -srcfolder "$app" -ov -format UDZO "$installer"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,174 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
|
||||
const COMMIT_PATTERN = /^[0-9a-f]{40}$/u
|
||||
const VERSION_PATTERN = /^\d+\.\d+\.\d+$/u
|
||||
const ARCHITECTURE_VERSION_PATTERN = /^\d{4}-\d{2}-\d{2}\.\d+$/u
|
||||
const DEVELOPMENT_ID_PATTERN = /^DEV-\d{8}-\d{3}$/u
|
||||
const CANONICAL_REPOSITORY = 'repo://guanghulab.com/code/bingshuo/hololake-system-architecture'
|
||||
const CANONICAL_REPOSITORY_ID = 'REPO-014'
|
||||
const CANONICAL_SOURCE_PATH = 'product-source/hololake-platform'
|
||||
const PROVENANCE_FILE_NAME = 'build-provenance.json'
|
||||
|
||||
function requireExactString(value, label, pattern) {
|
||||
if (typeof value !== 'string' || !pattern.test(value)) {
|
||||
throw new Error(`invalid ${label}`)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
export function buildInternalReleaseProvenance(input) {
|
||||
if (input.worktreeClean !== true) {
|
||||
throw new Error('internal release provenance requires a clean source worktree')
|
||||
}
|
||||
if (input.sourceRepository !== CANONICAL_REPOSITORY) {
|
||||
throw new Error('invalid source repository')
|
||||
}
|
||||
if (input.sourceRepositoryId !== CANONICAL_REPOSITORY_ID) {
|
||||
throw new Error('invalid source repository id')
|
||||
}
|
||||
if (input.sourcePath !== CANONICAL_SOURCE_PATH) {
|
||||
throw new Error('invalid source path')
|
||||
}
|
||||
|
||||
return {
|
||||
schema: 'hololake.internal-release-provenance/v1',
|
||||
architecture: {
|
||||
id: requireExactString(input.architectureId, 'architecture id', /^HLP-[A-Z0-9-]+$/u),
|
||||
version: requireExactString(input.architectureVersion, 'architecture version', ARCHITECTURE_VERSION_PATTERN),
|
||||
},
|
||||
build: {
|
||||
application_version: requireExactString(input.applicationVersion, 'application version', VERSION_PATTERN),
|
||||
development_id: requireExactString(input.developmentId, 'development id', DEVELOPMENT_ID_PATTERN),
|
||||
distribution: requireExactString(input.distribution, 'distribution', /^[a-z][a-z0-9-]+$/u),
|
||||
recorded_at: requireExactString(input.recordedAt, 'recorded at', /^\d{4}-\d{2}-\d{2}T/u),
|
||||
target_triple: requireExactString(input.targetTriple, 'target triple', /^[a-z0-9-]+$/u),
|
||||
},
|
||||
source: {
|
||||
branch: requireExactString(input.sourceBranch, 'source branch', /^[A-Za-z0-9._/-]+$/u),
|
||||
commit: requireExactString(input.sourceCommit, 'source commit', COMMIT_PATTERN),
|
||||
path: input.sourcePath,
|
||||
repository: input.sourceRepository,
|
||||
repository_id: input.sourceRepositoryId,
|
||||
tree: requireExactString(input.sourceTree, 'source tree', COMMIT_PATTERN),
|
||||
worktree_clean: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function expectEqual(actual, expected, label) {
|
||||
if (actual !== expected) throw new Error(`${label} mismatch`)
|
||||
}
|
||||
|
||||
export function validateInternalReleaseProvenance(provenance, expected) {
|
||||
if (!provenance || provenance.schema !== 'hololake.internal-release-provenance/v1') {
|
||||
throw new Error('provenance schema mismatch')
|
||||
}
|
||||
expectEqual(provenance.source?.commit, expected.sourceCommit, 'source commit')
|
||||
expectEqual(provenance.source?.tree, expected.sourceTree, 'source tree')
|
||||
expectEqual(provenance.source?.branch, expected.sourceBranch, 'source branch')
|
||||
expectEqual(provenance.source?.repository, expected.sourceRepository, 'source repository')
|
||||
expectEqual(provenance.source?.repository_id, expected.sourceRepositoryId, 'source repository id')
|
||||
expectEqual(provenance.source?.path, expected.sourcePath, 'source path')
|
||||
expectEqual(provenance.source?.worktree_clean, expected.worktreeClean, 'source worktree state')
|
||||
expectEqual(provenance.architecture?.id, expected.architectureId, 'architecture id')
|
||||
expectEqual(provenance.architecture?.version, expected.architectureVersion, 'architecture version')
|
||||
expectEqual(provenance.build?.application_version, expected.applicationVersion, 'application version')
|
||||
expectEqual(provenance.build?.development_id, expected.developmentId, 'development id')
|
||||
expectEqual(provenance.build?.distribution, expected.distribution, 'distribution')
|
||||
expectEqual(provenance.build?.target_triple, expected.targetTriple, 'target triple')
|
||||
return true
|
||||
}
|
||||
|
||||
function git(repositoryRoot, ...args) {
|
||||
return execFileSync('git', ['-C', repositoryRoot, ...args], { encoding: 'utf8' }).trim()
|
||||
}
|
||||
|
||||
async function repositoryExpectation({ repositoryRoot, developmentId, distribution, targetTriple }) {
|
||||
const architecture = JSON.parse(await readFile(join(repositoryRoot, 'routing/hololake-current-architecture.json'), 'utf8'))
|
||||
const application = JSON.parse(await readFile(join(repositoryRoot, CANONICAL_SOURCE_PATH, 'src-tauri/tauri.conf.json'), 'utf8'))
|
||||
return {
|
||||
sourceCommit: git(repositoryRoot, 'rev-parse', 'HEAD'),
|
||||
sourceTree: git(repositoryRoot, 'rev-parse', 'HEAD^{tree}'),
|
||||
sourceBranch: git(repositoryRoot, 'branch', '--show-current'),
|
||||
sourceRepository: CANONICAL_REPOSITORY,
|
||||
sourceRepositoryId: CANONICAL_REPOSITORY_ID,
|
||||
sourcePath: CANONICAL_SOURCE_PATH,
|
||||
architectureId: architecture.architecture_id,
|
||||
architectureVersion: architecture.version,
|
||||
applicationVersion: application.version,
|
||||
developmentId,
|
||||
distribution,
|
||||
targetTriple,
|
||||
worktreeClean: git(repositoryRoot, 'status', '--porcelain=v1', '--untracked-files=all') === '',
|
||||
}
|
||||
}
|
||||
|
||||
async function writeJsonAtomic(outputPath, value) {
|
||||
await mkdir(dirname(outputPath), { recursive: true })
|
||||
const temporary = `${outputPath}.${process.pid}.tmp`
|
||||
await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { flag: 'wx' })
|
||||
try {
|
||||
await rename(temporary, outputPath)
|
||||
} catch (error) {
|
||||
await rm(temporary, { force: true })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function findProvenanceFiles(directory) {
|
||||
const found = []
|
||||
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
||||
const entryPath = join(directory, entry.name)
|
||||
if (entry.isDirectory()) found.push(...await findProvenanceFiles(entryPath))
|
||||
else if (entry.isFile() && entry.name === PROVENANCE_FILE_NAME) found.push(entryPath)
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
function option(args, name) {
|
||||
const index = args.indexOf(name)
|
||||
if (index === -1 || !args[index + 1]) throw new Error(`${name} is required`)
|
||||
return args[index + 1]
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const [command, ...args] = process.argv.slice(2)
|
||||
if (!['generate', 'verify'].includes(command)) {
|
||||
throw new Error('Usage: internal-release-provenance.mjs <generate|verify> [options]')
|
||||
}
|
||||
|
||||
const repositoryRoot = git(process.cwd(), 'rev-parse', '--show-toplevel')
|
||||
const developmentId = option(args, '--development-id')
|
||||
const distribution = option(args, '--distribution')
|
||||
const targetTriple = option(args, '--target-triple')
|
||||
const expected = await repositoryExpectation({ repositoryRoot, developmentId, distribution, targetTriple })
|
||||
|
||||
if (command === 'generate') {
|
||||
const outputPath = resolve(option(args, '--output'))
|
||||
const provenance = buildInternalReleaseProvenance({
|
||||
...expected,
|
||||
recordedAt: new Date().toISOString(),
|
||||
})
|
||||
await writeJsonAtomic(outputPath, provenance)
|
||||
process.stdout.write(`${outputPath}\n`)
|
||||
return
|
||||
}
|
||||
|
||||
const bundlePath = resolve(option(args, '--bundle'))
|
||||
const provenanceFiles = await findProvenanceFiles(bundlePath)
|
||||
if (provenanceFiles.length !== 1) {
|
||||
throw new Error(`expected exactly one packaged ${PROVENANCE_FILE_NAME}, found ${provenanceFiles.length}`)
|
||||
}
|
||||
const provenance = JSON.parse(await readFile(provenanceFiles[0], 'utf8'))
|
||||
validateInternalReleaseProvenance(provenance, expected)
|
||||
process.stdout.write(`${provenanceFiles[0]}\n`)
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
await main()
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import {
|
||||
buildInternalReleaseProvenance,
|
||||
validateInternalReleaseProvenance,
|
||||
} from './internal-release-provenance.mjs'
|
||||
|
||||
const sourceCommit = '1'.repeat(40)
|
||||
const sourceTree = '2'.repeat(40)
|
||||
|
||||
function input(overrides = {}) {
|
||||
return {
|
||||
sourceCommit,
|
||||
sourceTree,
|
||||
sourceBranch: 'main',
|
||||
sourceRepository: 'repo://guanghulab.com/code/bingshuo/hololake-system-architecture',
|
||||
sourceRepositoryId: 'REPO-014',
|
||||
sourcePath: 'product-source/hololake-platform',
|
||||
architectureId: 'HLP-CURRENT-ARCH-001',
|
||||
architectureVersion: '2026-08-12.16',
|
||||
applicationVersion: '0.4.6',
|
||||
developmentId: 'DEV-20260811-010',
|
||||
distribution: 'local-candidate',
|
||||
targetTriple: 'aarch64-apple-darwin',
|
||||
recordedAt: '2026-08-12T07:00:00+08:00',
|
||||
worktreeClean: true,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
test('builds a source-bound local candidate provenance record', () => {
|
||||
assert.deepEqual(buildInternalReleaseProvenance(input()), {
|
||||
schema: 'hololake.internal-release-provenance/v1',
|
||||
architecture: {
|
||||
id: 'HLP-CURRENT-ARCH-001',
|
||||
version: '2026-08-12.16',
|
||||
},
|
||||
build: {
|
||||
application_version: '0.4.6',
|
||||
development_id: 'DEV-20260811-010',
|
||||
distribution: 'local-candidate',
|
||||
recorded_at: '2026-08-12T07:00:00+08:00',
|
||||
target_triple: 'aarch64-apple-darwin',
|
||||
},
|
||||
source: {
|
||||
branch: 'main',
|
||||
commit: sourceCommit,
|
||||
path: 'product-source/hololake-platform',
|
||||
repository: 'repo://guanghulab.com/code/bingshuo/hololake-system-architecture',
|
||||
repository_id: 'REPO-014',
|
||||
tree: sourceTree,
|
||||
worktree_clean: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('refuses to issue provenance for a dirty source tree', () => {
|
||||
assert.throws(
|
||||
() => buildInternalReleaseProvenance(input({ worktreeClean: false })),
|
||||
/clean source worktree/,
|
||||
)
|
||||
})
|
||||
|
||||
test('validates exact source, architecture, application and development bindings', () => {
|
||||
const provenance = buildInternalReleaseProvenance(input())
|
||||
|
||||
assert.equal(validateInternalReleaseProvenance(provenance, input()), true)
|
||||
assert.throws(
|
||||
() => validateInternalReleaseProvenance(provenance, input({ sourceCommit: '3'.repeat(40) })),
|
||||
/source commit mismatch/,
|
||||
)
|
||||
assert.throws(
|
||||
() => validateInternalReleaseProvenance(provenance, input({ architectureVersion: '2026-08-12.17' })),
|
||||
/architecture version mismatch/,
|
||||
)
|
||||
})
|
||||
|
|
@ -4,7 +4,7 @@ import test from 'node:test'
|
|||
|
||||
const readJson = async path => JSON.parse(await readFile(path, 'utf8'))
|
||||
|
||||
test('local native candidate has an isolated macOS identity', async () => {
|
||||
test('local native candidate has an isolated macOS package identity', async () => {
|
||||
const [base, candidate, packageJson] = await Promise.all([
|
||||
readJson('src-tauri/tauri.conf.json'),
|
||||
readJson('src-tauri/tauri.local-candidate.conf.json'),
|
||||
|
|
@ -20,4 +20,10 @@ test('local native candidate has an isolated macOS identity', async () => {
|
|||
packageJson.scripts['package:local-candidate:macos'],
|
||||
/tauri\.local-candidate\.conf\.json/,
|
||||
)
|
||||
assert.match(packageJson.scripts.build, /--configLoader runner/)
|
||||
assert.match(packageJson.scripts['test:native-core'], /--configLoader runner/)
|
||||
assert.match(
|
||||
await readFile('scripts/run-hololake-native-quality-gate.sh', 'utf8'),
|
||||
/vite build --configLoader runner[\s\S]*vitest run --configLoader runner[\s\S]*--coverage --configLoader runner/,
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -119,12 +119,12 @@ run_gate frontend_zero_warning_lint \
|
|||
run_gate frontend_type_contract \
|
||||
run_package_tool tsc -b
|
||||
run_gate frontend_build \
|
||||
run_package_tool vite build
|
||||
run_package_tool vite build --configLoader runner
|
||||
run_gate frontend_unit_and_integration_tests \
|
||||
run_package_tool vitest run
|
||||
run_package_tool vitest run --configLoader runner
|
||||
run_gate auditable_native_core_lines_and_functions_100 \
|
||||
run_package_tool vitest run \
|
||||
--config vitest.guanghu-native.config.ts --coverage
|
||||
--config vitest.guanghu-native.config.ts --coverage --configLoader runner
|
||||
run_gate rust_format \
|
||||
cargo fmt --all --manifest-path "${repository_root}/src-tauri/Cargo.toml" -- --check
|
||||
run_gate rust_unit_and_integration_tests \
|
||||
|
|
|
|||
|
|
@ -33,8 +33,8 @@ const packageManagerExec = process.env.npm_execpath
|
|||
const isJsExecpath = packageManagerExec && /\.[mc]?js$/i.test(packageManagerExec)
|
||||
const command = isJsExecpath ? process.execPath : 'pnpm'
|
||||
const baseCommandArgs = isJsExecpath
|
||||
? [packageManagerExec, 'exec', 'vitest', 'run', '--coverage']
|
||||
: ['exec', 'vitest', 'run', '--coverage']
|
||||
? [packageManagerExec, 'exec', 'vitest', 'run', '--coverage', '--configLoader', 'runner']
|
||||
: ['exec', 'vitest', 'run', '--coverage', '--configLoader', 'runner']
|
||||
const clearCacheCommandArgs = isJsExecpath
|
||||
? [packageManagerExec, 'exec', 'vitest', '--clearCache']
|
||||
: ['exec', 'vitest', '--clearCache']
|
||||
|
|
|
|||
|
|
@ -360,6 +360,30 @@ describe('AiWorkspace', () => {
|
|||
expect(screen.queryByRole('button', { name: 'Current work' })).toBeNull()
|
||||
})
|
||||
|
||||
it('does not rewrite unchanged conversation settings when the workspace opens', async () => {
|
||||
const onConversationSettingsChange = vi.fn()
|
||||
|
||||
render(
|
||||
<AiWorkspace
|
||||
open
|
||||
mode="side"
|
||||
aiAgentsStatus={installedStatuses()}
|
||||
aiModelProviders={providers}
|
||||
conversationSettings={[
|
||||
{ id: 'live-chat', title: 'Current work', target_id: null, archived: false },
|
||||
]}
|
||||
vaultPath="/tmp/vault"
|
||||
onClose={vi.fn()}
|
||||
onConversationSettingsChange={onConversationSettingsChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: 'Current work' })).toBeTruthy()
|
||||
})
|
||||
expect(onConversationSettingsChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('recovers stored sessions that no longer have conversation metadata', async () => {
|
||||
setAiWorkspaceSessionMessages('orphaned-chat', [{
|
||||
userMessage: 'Recovered prompt',
|
||||
|
|
|
|||
|
|
@ -197,6 +197,20 @@ function conversationsToSettings(conversations: AiConversation[]): AiWorkspaceCo
|
|||
}))
|
||||
}
|
||||
|
||||
function conversationSettingsEqual(
|
||||
current: AiWorkspaceConversationSetting[],
|
||||
stored: AiWorkspaceConversationSetting[] | null | undefined,
|
||||
): boolean {
|
||||
if (!stored || current.length !== stored.length) return false
|
||||
return current.every((conversation, index) => {
|
||||
const persisted = stored[index]
|
||||
return conversation.archived === (persisted.archived === true)
|
||||
&& conversation.id === persisted.id
|
||||
&& conversation.target_id === (persisted.target_id?.trim() || null)
|
||||
&& conversation.title === persisted.title
|
||||
})
|
||||
}
|
||||
|
||||
export function activeConversationForState(
|
||||
conversations: AiConversation[],
|
||||
activeId: ConversationId,
|
||||
|
|
@ -370,10 +384,12 @@ function initialActiveId(conversations: AiConversation[], requestedId: Conversat
|
|||
function useConversationSettingsPersistence({
|
||||
conversations,
|
||||
onSettingsChange,
|
||||
settings,
|
||||
settingsReady,
|
||||
}: {
|
||||
conversations: AiConversation[]
|
||||
onSettingsChange?: (conversations: AiWorkspaceConversationSetting[]) => void
|
||||
settings?: AiWorkspaceConversationSetting[] | null
|
||||
settingsReady: boolean
|
||||
}) {
|
||||
const onSettingsChangeRef = useRef(onSettingsChange)
|
||||
|
|
@ -384,8 +400,10 @@ function useConversationSettingsPersistence({
|
|||
|
||||
useEffect(() => {
|
||||
if (!settingsReady) return
|
||||
onSettingsChangeRef.current?.(conversationsToSettings(conversations))
|
||||
}, [conversations, settingsReady])
|
||||
const nextSettings = conversationsToSettings(conversations)
|
||||
if (conversationSettingsEqual(nextSettings, settings)) return
|
||||
onSettingsChangeRef.current?.(nextSettings)
|
||||
}, [conversations, settings, settingsReady])
|
||||
}
|
||||
|
||||
function useTitleConversationFromAnswer(setConversations: SetConversations) {
|
||||
|
|
@ -490,7 +508,7 @@ export function useConversations({
|
|||
|
||||
const titleConversationFromAnswer = useTitleConversationFromAnswer(setConversations)
|
||||
const updateDefaultConversationTargets = useUpdateDefaultConversationTargets(setConversations)
|
||||
useConversationSettingsPersistence({ conversations, onSettingsChange, settingsReady })
|
||||
useConversationSettingsPersistence({ conversations, onSettingsChange, settings, settingsReady })
|
||||
|
||||
return {
|
||||
activeId, addConversation, archiveConversation, closeConversation, conversations, deleteConversation, forkConversation,
|
||||
|
|
|
|||
|
|
@ -178,4 +178,28 @@ describe('aiWorkspaceSessionStore', () => {
|
|||
expect(store.aiWorkspaceSessionSnapshot('native-chat').messages).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
it('does not rewrite native history when hydration only reads it', async () => {
|
||||
isTauriState.value = true
|
||||
invokeMock.mockImplementation(async (command: string) => {
|
||||
if (command === 'get_ai_workspace_sessions') {
|
||||
return {
|
||||
'native-chat': {
|
||||
messages: [{ userMessage: 'Native', actions: [], response: 'History', id: 'native-message' }],
|
||||
status: 'done',
|
||||
},
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const store = await import('./aiWorkspaceSessionStore')
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(store.aiWorkspaceSessionSnapshot('native-chat').messages).toHaveLength(1)
|
||||
})
|
||||
await new Promise(resolve => setTimeout(resolve, 300))
|
||||
|
||||
expect(invokeMock).not.toHaveBeenCalledWith('save_ai_workspace_sessions', expect.anything())
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -160,7 +160,6 @@ async function syncFromNativeStorage(): Promise<void> {
|
|||
const mergedSessions = mergeStoredSessions(sessionStore.getSnapshot(), nativeSessions)
|
||||
sessionStore.replaceSnapshot(mergedSessions)
|
||||
sessionStore.writeStoredSnapshot(mergedSessions)
|
||||
if (Object.keys(mergedSessions).length > 0) scheduleNativeSessionsWrite(mergedSessions)
|
||||
}
|
||||
|
||||
function ensureSessionStoreSync(): void {
|
||||
|
|
|
|||
|
|
@ -14,11 +14,14 @@ import {
|
|||
type Dirent,
|
||||
} from 'fs'
|
||||
import os from 'os'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { defineConfig, type Plugin } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import matter from 'gray-matter'
|
||||
|
||||
const configDirectory = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
// --- Vault API middleware (dev only) ---
|
||||
|
||||
interface VaultEntry {
|
||||
|
|
@ -962,7 +965,7 @@ export default defineConfig({
|
|||
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
'@': path.resolve(configDirectory, './src'),
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -972,7 +975,7 @@ export default defineConfig({
|
|||
define: {
|
||||
...(process.env.CI || (process.env.TAURI_PLATFORM && !process.env.TAURI_DEBUG)
|
||||
? {}
|
||||
: { __DEMO_VAULT_PATH__: JSON.stringify(path.resolve(__dirname, 'demo-vault-v2')) }),
|
||||
: { __DEMO_VAULT_PATH__: JSON.stringify(path.resolve(configDirectory, 'demo-vault-v2')) }),
|
||||
},
|
||||
|
||||
// Prevent vite from obscuring Rust errors
|
||||
|
|
@ -993,7 +996,7 @@ export default defineConfig({
|
|||
|
||||
build: {
|
||||
rollupOptions: process.env.VITE_HOLOLAKE_DISTRIBUTION === 'team-foundation'
|
||||
? { input: path.resolve(__dirname, 'team-foundation.html') }
|
||||
? { input: path.resolve(configDirectory, 'team-foundation.html') }
|
||||
: undefined,
|
||||
// Tauri uses Chromium on Windows and WebKit on macOS/Linux
|
||||
target: process.env.TAURI_PLATFORM === 'windows' ? 'chrome105' : 'safari13',
|
||||
|
|
|
|||
Loading…
Reference in a new issue