Compare commits

..
17 changed files with 460 additions and 26 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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