Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions .github/workflows/publish-unsigned-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ on:
description: New prerelease tag; an existing release is never overwritten
required: true
type: string
default: v0.5.0-beta.3
default: v0.5.0-beta.4
preview_title:
description: Public prerelease title
required: true
type: string
default: Parlyn Engine v0.5.0 Beta 3 (unsigned)
default: Parlyn Engine v0.5.0 Beta 4 (unsigned)

permissions:
contents: write
Expand All @@ -29,8 +29,8 @@ jobs:
env:
CSC_IDENTITY_AUTO_DISCOVERY: false
GH_TOKEN: ${{ github.token }}
PREVIEW_TAG: ${{ inputs.preview_tag || 'v0.5.0-beta.3' }}
PREVIEW_TITLE: ${{ inputs.preview_title || 'Parlyn Engine v0.5.0 Beta 3 (unsigned)' }}
PREVIEW_TAG: ${{ inputs.preview_tag || 'v0.5.0-beta.4' }}
PREVIEW_TITLE: ${{ inputs.preview_title || 'Parlyn Engine v0.5.0 Beta 4 (unsigned)' }}

steps:
- name: Checkout
Expand Down Expand Up @@ -68,11 +68,12 @@ jobs:
shell: pwsh
run: |
@"
# Parlyn Engine v0.5.0 Beta 3
# Parlyn Engine v0.5.0 Beta 4

This unsigned Windows beta contains the completed automated Phase 1
foundation candidate. It restores the packaged Electron host bridge
required for project creation and saving, and includes atomic persistence,
required for project creation and saving. It adds safe project closing,
recoverable Recycle Bin deletion and explicit prerelease filenames alongside atomic persistence,
hardened Node and Undo/Redo invariants, visible editor errors, tighter
desktop trust boundaries and corrected packaged toolbar branding.

Expand All @@ -87,7 +88,7 @@ jobs:
remain pending. This release is not SignPath-signed.

Maintainer acceptance instructions:
https://github.com/WebCrew/Parlyn-Engine/blob/main/docs/MAINTAINER-ACCEPTANCE-v0.5.0-beta.3.md
https://github.com/WebCrew/Parlyn-Engine/blob/main/docs/MAINTAINER-ACCEPTANCE-v0.5.0-beta.4.md
"@ | Set-Content -LiteralPath release/PREVIEW-NOTES.md -Encoding utf8

- name: Publish GitHub pre-release
Expand Down
5 changes: 4 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased — Foundation Stabilization

- Added explicit Close Project behavior that clears the privileged project session and protects unsaved scene changes.
- Added exact-name-confirmed project deletion through the operating system Recycle Bin; Parlyn never permanently deletes the active project.
- Adopted full Semantic Version prerelease names in application metadata and Windows installer filenames.
- Restored the sandboxed Electron preload bridge so packaged project creation and saving work again.
- Added sandboxed preload contract and real Electron smoke tests that verify every desktop host method before Windows packaging.
- Added a reproducible 64-bit Windows NSIS installer foundation.
Expand Down Expand Up @@ -41,7 +44,7 @@
- Bounded loaded scenes to 10,000 nodes and 256 hierarchy levels.
- Reworked plain-JSON validation to avoid recursive call-stack exhaustion.
- Added automated Node hierarchy and Undo/Redo invariant checks.
- Added a repeatable Beta 3 maintainer acceptance checklist for the Phase 1 completion gate.
- Added a repeatable Beta 4 maintainer acceptance checklist for the Phase 1 completion gate.
- Generalized the non-overwriting unsigned preview workflow for explicit beta releases.


Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ See [`docs/WINDOWS-DISTRIBUTION.md`](docs/WINDOWS-DISTRIBUTION.md) for signing,
verification and the maintainer acceptance test.

The current Phase 1 acceptance candidate uses the repeatable
[`v0.5.0-beta.3 maintainer checklist`](docs/MAINTAINER-ACCEPTANCE-v0.5.0-beta.3.md).
[`v0.5.0-beta.4 maintainer checklist`](docs/MAINTAINER-ACCEPTANCE-v0.5.0-beta.4.md).

### Code signing policy

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Parlyn Engine v0.5.0 Beta 3 — Maintainer Acceptance
# Parlyn Engine v0.5.0 Beta 4 — Maintainer Acceptance

This is the repeatable Phase 1 acceptance pass. Perform it on the published
`v0.5.0-beta.3` artifact, not on a development checkout or an older beta.
`v0.5.0-beta.4` artifact, not on a development checkout or an older beta.

Smart App Control and normal Windows security settings remain enabled. This
preview is intentionally unsigned while the SignPath Foundation application is
Expand All @@ -19,11 +19,11 @@ Record these values in Issue #23 after the pass:

## 1. Download and integrity

1. Download the installer and `SHA256SUMS.txt` from the Beta 3 prerelease.
1. Download the installer and `SHA256SUMS.txt` from the Beta 4 prerelease.
2. In PowerShell, run:

```powershell
Get-FileHash "$env:USERPROFILE\Downloads\Parlyn-Engine-Setup-0.5.0-x64.exe" -Algorithm SHA256
Get-FileHash "$env:USERPROFILE\Downloads\Parlyn-Engine-Setup-0.5.0-beta.4-x64.exe" -Algorithm SHA256
```

3. Confirm the displayed hash matches `SHA256SUMS.txt`.
Expand All @@ -37,12 +37,16 @@ Record these values in Issue #23 after the pass:

## 3. Project and persistence

1. Create a project named `Parlyn Beta 3 Test` in a new empty parent folder.
1. Create a project named `Parlyn Beta 4 Test` in a new empty parent folder.
2. Add one 2.5D Sprite and one 3D Mesh.
3. Rename both nodes and change their positions.
4. Save the scene, close Parlyn, reopen the project and confirm names and positions survived.
5. Open **World**, confirm the project world name and seed appear, then use **Save World**.
6. Open the bundled `samples/Parlyn-Test-Project` and confirm its hierarchy and imported asset appear.
7. Make an unsaved change, choose **Close**, then cancel the warning and confirm the project remains open.
8. Choose **Close** again, use **Save & Continue**, then reopen the project and confirm the last change survived.
9. Create a disposable project named `Parlyn Delete Test`, choose **Delete**, confirm the button remains disabled for a wrong name, then enter the exact name.
10. Confirm the editor returns to `Loose Scene`, the project folder leaves its original location and can be found in the Windows Recycle Bin.

## 4. Hierarchy, transforms and history

Expand All @@ -68,8 +72,8 @@ Record these values in Issue #23 after the pass:

## 7. Uninstall preservation

1. Close Parlyn and uninstall Beta 3 normally.
2. Confirm the separately created `Parlyn Beta 3 Test` project still exists with its files intact.
1. Close Parlyn and uninstall Beta 4 normally.
2. Confirm the separately created `Parlyn Beta 4 Test` project still exists with its files intact.

Phase 1 passes only when every required section succeeds or a discovered defect
is fixed and the affected section is repeated against a new artifact.
2 changes: 1 addition & 1 deletion docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Already present:

Completion gate:

- complete the repeatable `v0.5.0-beta.3` maintainer test pass and record the result in Issue #23.
- complete the repeatable `v0.5.0-beta.4` maintainer test pass and record the result in Issue #23.

## Phase 2 — Editor foundation

Expand Down
2 changes: 1 addition & 1 deletion docs/TESTING-v0.5.0.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Parlyn Engine v0.5.0 Test Guide

For the Phase 1 release-candidate acceptance pass, use the shorter artifact-specific
[`MAINTAINER-ACCEPTANCE-v0.5.0-beta.3.md`](MAINTAINER-ACCEPTANCE-v0.5.0-beta.3.md)
[`MAINTAINER-ACCEPTANCE-v0.5.0-beta.4.md`](MAINTAINER-ACCEPTANCE-v0.5.0-beta.4.md)
and record the result in Issue #23. The guide below remains the detailed feature reference.

This test focuses on the two new foundations introduced in v0.5.0: viewport transform gizmos and the module lifecycle.
Expand Down
4 changes: 2 additions & 2 deletions docs/WINDOWS-DISTRIBUTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,6 @@ the maintainer's Windows machine. Public trusted-signing acceptance remains
pending separately from the resolved development-binary blocker.

The full Phase 1 functional acceptance candidate is the unsigned
`v0.5.0-beta.3` prerelease. Its repeatable checklist is documented in
[`MAINTAINER-ACCEPTANCE-v0.5.0-beta.3.md`](MAINTAINER-ACCEPTANCE-v0.5.0-beta.3.md),
`v0.5.0-beta.4` prerelease. Its repeatable checklist is documented in
[`MAINTAINER-ACCEPTANCE-v0.5.0-beta.4.md`](MAINTAINER-ACCEPTANCE-v0.5.0-beta.4.md),
and the result is recorded in Issue #23 before Phase 1 can be marked complete.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "parlyn-engine",
"version": "0.5.0",
"version": "0.5.0-beta.4",
"private": true,
"description": "Parlyn Engine - open-source 2.5D-first game engine editor",
"author": "Parlyn Engine contributors",
Expand Down
4 changes: 3 additions & 1 deletion scripts/check-desktop-boundaries.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ const { resolveExistingProjectPath, resolveWritableProjectPath } = require('../s
assert.match(main, /will-navigate/);
assert.match(main, /secureHandle\('parlyn:app:get-info'/);
assert.match(main, /secureHandle\('parlyn:project:open'/);
assert.match(main, /secureHandle\('parlyn:project:close'/);
assert.match(main, /secureHandle\('parlyn:project:delete'/);

let exposedHost = null;
vm.runInNewContext(preload, {
Expand All @@ -69,7 +71,7 @@ const { resolveExistingProjectPath, resolveWritableProjectPath } = require('../s
}
}, { filename:'src/main/preload.js' });
assert.ok(exposedHost, 'Preload must expose window.parlynHost.');
for (const method of ['getAppInfo','createProject','openProject','saveProjectScene','saveProjectWorld','saveSceneAs','openScene','importAssets']) {
for (const method of ['getAppInfo','createProject','openProject','closeProject','deleteProject','saveProjectScene','saveProjectWorld','saveSceneAs','openScene','importAssets']) {
assert.equal(typeof exposedHost[method], 'function', `Preload host is missing ${method}().`);
}

Expand Down
2 changes: 2 additions & 0 deletions scripts/check-electron-preload.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const methods = [
'getAppInfo',
'createProject',
'openProject',
'closeProject',
'deleteProject',
'saveProjectScene',
'saveProjectWorld',
'saveSceneAs',
Expand Down
61 changes: 61 additions & 0 deletions scripts/check-project-session.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
const assert = require('assert/strict');
const path = require('path');
const { ProjectSession } = require('../src/main/ProjectSession');

(async () => {
const projectRoot = path.resolve('test-project');
const trashed = [];
let loadFailure = null;
let trashFailure = null;
const session = new ProjectSession({
async loadProject(root) {
if (loadFailure) throw loadFailure;
assert.equal(root, projectRoot);
return { format:'parlyn-project', version:1, name:'Safety Test' };
},
async trashItem(root) { if (trashFailure) throw trashFailure; trashed.push(root); },
isProtectedRoot:root => root === path.resolve('protected-user-folder')
});

assert.deepEqual(session.close(), { ok:false, reason:'no-project' });
assert.deepEqual(await session.moveToTrash('Safety Test'), { ok:false, reason:'no-project' });
assert.throws(() => session.activate('relative/project'), /absolute path/);

session.activate(projectRoot);
assert.equal(session.activeProjectRoot, projectRoot);
const closed = session.close();
assert.deepEqual(closed, { ok:true, projectRoot });
assert.equal(session.activeProjectRoot, null);

session.activate(projectRoot);
await assert.rejects(session.moveToTrash('wrong name'), /exact project name/);
assert.equal(session.activeProjectRoot, projectRoot);
assert.deepEqual(trashed, []);

loadFailure = new Error('Invalid project file');
await assert.rejects(session.moveToTrash('Safety Test'), /Invalid project file/);
assert.equal(session.activeProjectRoot, projectRoot);
assert.deepEqual(trashed, []);

loadFailure = null;
trashFailure = new Error('Recycle Bin unavailable');
await assert.rejects(session.moveToTrash('Safety Test'), /Recycle Bin unavailable/);
assert.equal(session.activeProjectRoot, projectRoot);
assert.deepEqual(trashed, []);

trashFailure = null;
const deleted = await session.moveToTrash('Safety Test');
assert.deepEqual(deleted, { ok:true, projectRoot, projectName:'Safety Test' });
assert.equal(session.activeProjectRoot, null);
assert.deepEqual(trashed, [projectRoot]);

session.activate(path.parse(projectRoot).root);
await assert.rejects(session.moveToTrash('Safety Test'), /protected system or user folder/);
session.activate(path.resolve('protected-user-folder'));
await assert.rejects(session.moveToTrash('Safety Test'), /protected system or user folder/);

console.log('Project session close and Recycle Bin contract check passed.');
})().catch((error) => {
console.error(error);
process.exitCode = 1;
});
7 changes: 5 additions & 2 deletions scripts/check.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const required = [
'src/main/documentFiles.mjs',
'src/main/ipcSecurity.js',
'src/main/projectPaths.js',
'src/main/ProjectSession.js',
'src/main/preload.js',
'src/renderer/index.html',
'src/renderer/app.mjs',
Expand All @@ -28,13 +29,14 @@ const required = [
'docs/SMART-SYSTEMS.md',
'docs/AUTHENTICATION.md',
'docs/WINDOWS-DISTRIBUTION.md',
'docs/MAINTAINER-ACCEPTANCE-v0.5.0-beta.3.md',
'docs/MAINTAINER-ACCEPTANCE-v0.5.0-beta.4.md',
'build/icon.ico',
'.github/workflows/windows-installer.yml',
'scripts/check-smart-systems.mjs',
'scripts/check-core-persistence.mjs',
'scripts/check-core-invariants.mjs',
'scripts/check-desktop-boundaries.cjs',
'scripts/check-project-session.cjs',
'scripts/check-electron-preload.cjs',
'scripts/check-windows-distribution.cjs',
'scripts/verify-windows-artifacts.ps1'
Expand All @@ -46,7 +48,7 @@ for (const rel of required) {

const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
if (pkg.name !== 'parlyn-engine') throw new Error('Unexpected package name.');
if (pkg.version !== '0.5.0') throw new Error(`Unexpected package version: ${pkg.version}`);
if (pkg.version !== '0.5.0-beta.4') throw new Error(`Unexpected package version: ${pkg.version}`);

function walk(dir) {
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
Expand All @@ -65,6 +67,7 @@ cp.execFileSync(process.execPath, [path.join(root, 'scripts/check-smart-systems.
cp.execFileSync(process.execPath, [path.join(root, 'scripts/check-core-persistence.mjs')], { stdio: 'inherit' });
cp.execFileSync(process.execPath, [path.join(root, 'scripts/check-core-invariants.mjs')], { stdio: 'inherit' });
cp.execFileSync(process.execPath, [path.join(root, 'scripts/check-desktop-boundaries.cjs')], { stdio: 'inherit' });
cp.execFileSync(process.execPath, [path.join(root, 'scripts/check-project-session.cjs')], { stdio: 'inherit' });
cp.execFileSync(process.execPath, [path.join(root, 'scripts/check-windows-distribution.cjs')], { stdio: 'inherit' });

console.log(`Parlyn structure check passed (${codeFiles.length} JavaScript modules checked).`);
51 changes: 51 additions & 0 deletions src/main/ProjectSession.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const path = require('path');

class ProjectSession {
#activeProjectRoot = null;

constructor({ loadProject, trashItem, isProtectedRoot = () => false }) {
if (typeof loadProject !== 'function') throw new TypeError('ProjectSession requires loadProject().');
if (typeof trashItem !== 'function') throw new TypeError('ProjectSession requires trashItem().');
if (typeof isProtectedRoot !== 'function') throw new TypeError('ProjectSession isProtectedRoot must be a function.');
this.loadProject = loadProject;
this.trashItem = trashItem;
this.isProtectedRoot = isProtectedRoot;
}

get activeProjectRoot() {
return this.#activeProjectRoot;
}

activate(projectRoot) {
if (typeof projectRoot !== 'string' || !path.isAbsolute(projectRoot)) {
throw new TypeError('Active project root must be an absolute path.');
}
this.#activeProjectRoot = projectRoot;
return projectRoot;
}

close() {
if (!this.#activeProjectRoot) return { ok:false, reason:'no-project' };
const projectRoot = this.#activeProjectRoot;
this.#activeProjectRoot = null;
return { ok:true, projectRoot };
}

async moveToTrash(confirmationName) {
const projectRoot = this.#activeProjectRoot;
if (!projectRoot) return { ok:false, reason:'no-project' };
if (path.parse(projectRoot).root === projectRoot || this.isProtectedRoot(projectRoot)) {
throw new Error('Parlyn refuses to move a protected system or user folder to the Recycle Bin.');
}
const project = await this.loadProject(projectRoot);
if (typeof confirmationName !== 'string' || confirmationName !== project.name) {
throw new Error(`Type the exact project name "${project.name}" to move it to the Recycle Bin.`);
}
if (this.#activeProjectRoot !== projectRoot) throw new Error('The active project changed before deletion. Try again.');
await this.trashItem(projectRoot);
if (this.#activeProjectRoot === projectRoot) this.#activeProjectRoot = null;
return { ok:true, projectRoot, projectName:project.name };
}
}

module.exports = { ProjectSession };
Loading
Loading