Skip to content
Draft
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
59 changes: 59 additions & 0 deletions docs/experimental-library-integration-ledger.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Experimental Library & Lineage Integration Ledger — 2026-10-09

> **NOT A RELEASE.** Review branch only. Parent: `integration/modly-next-2026-10-09`
> (upstream v0.4.3 + separate candidate fixes). This port has **not** passed
> combined npm/TypeScript/Python tests or a Windows desktop smoke test.

## Source PRs ported as separate commits

| Upstream PR | Feature | Port note |
| --- | --- | --- |
| [#242](https://github.com/lightningpixel/modly/pull/242) | Animated GLB clips | Corrected malformed upstream JSX fallback `{clip.name || }` on our branch, then incorporated the MotionBar replacement from #245. |
| [#243](https://github.com/lightningpixel/modly/pull/243) | Library thumbnails and width persistence | Reconciled GeneratePage state with current mesh-export UI. |
| [#244](https://github.com/lightningpixel/modly/pull/244) | Normalize manifest object-shaped input slots | Merged 3 regression cases into the newer existing test file. |
| [#245](https://github.com/lightningpixel/modly/pull/245) | Motion bar, tag UX, export metadata, panel zoom/resize | Reconciled viewer and parameter controls with 0.4.3; intentionally did not overwrite newer `package-lock.json`. Added required free-text param renderer. |
| [#247](https://github.com/lightningpixel/modly/pull/247) | Taller resizable Library | Reconciled stored height and state refs with current viewer. |
| [#248](https://github.com/lightningpixel/modly/pull/248) | Wider Library, direct open, hover-preview | Reconciled preview type imports. |
| [#249](https://github.com/lightningpixel/modly/pull/249) | Gallery grouped by project and lineage | Integrated updated asset-index tests in current suite. |

**Credits:** The work is based on PRs submitted by OmarB97 to Lightning Pixel's
Modly repository. The adaptations in this fork do not supersede their source PRs.

## Not ported: requires an independent functional adaptation

- **#246 (Chat + asset context):** its old Ollama agent backend does not match
current 0.4.3 llama.cpp/chat architecture. The workflow runner input routing
also changed. Do not paste the old agent router or silently drop context.
- **#250 (Optimizer preset semantics):** the source PR targets a legacy
`processor.ts`, whereas 0.4.3 uses a Python workflow adapter feeding a shared
mesh-op registry. A "full detail/no simplification" preset needs a reviewed
end-to-end contract that does not alter unrelated direct mesh API calls.
- **#251 (Workflow parameter binding and library sync):** stack depends on the
#250 behavior. Port only after defining stable override precedence and tests.
- **#252 (Artist intent record and texture hot reload):** much larger divergent
patch (workflow and API changes, new watcher services, extra proof assets).
Requires separate rebase and validation against 0.4.3 and #246/#251.

## Validation checklist — currently not executed on the combined branch

- [ ] Install dependencies reproducibly and run `npm run lint`.
- [ ] `npm run test:node` and `npm run test:py`.
- [ ] `npx tsc --noEmit -p tsconfig.node.json`.
- [ ] `npm run build`.
- [ ] Native Windows checks: launch Electron and load GLB with 1 and 3 clips,
switch motion bar clips, unload, and verify mixer teardown.
- [ ] Check Library thumbnails, previews, project/lineage filters and persistent
panel size across restart, with missing/corrupt metadata.
- [ ] Export rigged GLB/OBJ/STL/PLY, verify transforms/tags and project lineage.
- [ ] Retry free-text params and object-shaped manifest inputs.
- [ ] Verify source #370 destructive-storage safeguards and related tests in
the final combined branch.
- [ ] Review source #266 local API security separately (not included).

## Integration rules

1. Keep the user's `main` and upstream untouched.
2. Pin reviewed source PR/commit IDs; credit original contributors.
3. A matching patch hunk is **not** a passing test.
4. Do not merge this branch into Modly Next before validating the full
app and resolving the remaining integration ledger.
163 changes: 162 additions & 1 deletion electron/main/artifact-registry-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
normalizeWorkspaceAssetPath,
openWorkspaceAssetLibraryEntry,
readWorkspaceAssetLibraryEntry,
readWorkspaceAssetLibraryThumbnail,
registerWorkspaceAssetLibraryIpcHandlers,
} from './artifact-registry-service.ts'

Expand All @@ -22,7 +23,8 @@ async function withWorkspace(run: (workspaceDir: string) => Promise<void>) {
}
}

test('normalizes only workspace-relative paths under allowed Workflows and Exports roots', () => withWorkspace(async (workspaceDir) => {
test('normalizes only workspace-relative paths under allowed model roots', () => withWorkspace(async (workspaceDir) => {
assert.equal(normalizeWorkspaceAssetPath(workspaceDir, 'Default/hero.glb').workspacePath, 'Default/hero.glb')
assert.equal(normalizeWorkspaceAssetPath(workspaceDir, 'Workflows/checkpoints/hero.glb').workspacePath, 'Workflows/checkpoints/hero.glb')
assert.equal(normalizeWorkspaceAssetPath(workspaceDir, 'Exports/hero.glb').workspacePath, 'Exports/hero.glb')
assert.throws(() => normalizeWorkspaceAssetPath(workspaceDir, '../secret.glb'), /traversal|escape|relative/i)
Expand Down Expand Up @@ -76,6 +78,50 @@ test('registers a generated scene directory through its canonical manifest artif
assert.equal(scene && scene.state, 'ready')
}))

test('indexes generated models and reads project, structural tags, creation time, and lineage from model sidecars', () => withWorkspace(async (workspaceDir) => {
await mkdir(path.join(workspaceDir, 'Default'), { recursive: true })
await mkdir(path.join(workspaceDir, 'Exports/adventure'), { recursive: true })
await writeFile(path.join(workspaceDir, 'Default/hero.glb'), 'glb')
await writeFile(path.join(workspaceDir, 'Default/hero.tags.json'), JSON.stringify({
name: 'Hero',
project: null,
tags: ['mid-poly'],
created: '2026-06-15T10:00:00.000Z',
}))
await writeFile(path.join(workspaceDir, 'Exports/adventure/hero-rigged.glb'), 'glb')
await writeFile(path.join(workspaceDir, 'Exports/adventure/hero-rigged.tags.json'), JSON.stringify({
name: 'Hero Rigged',
project: 'Adventure',
tags: ['rigged', 'animated', 'clip-hero-walk', 'mid-poly'],
created: '2026-06-16T10:00:00.000Z',
derived_from: {
parent: { path: 'Default/hero.glb', name: 'Hero' },
root: { path: 'Default/hero.glb', name: 'Hero' },
},
}))

const result = await listWorkspaceAssetLibrary({ workspaceDir })
assert.equal(result.success, true)
if (!result.success) return
assert.deepEqual(result.entries.map((entry) => entry.workspacePath), [
'Default/hero.glb',
'Exports/adventure/hero-rigged.glb',
])

const root = result.entries[0]
const derived = result.entries[1]
assert.equal(root.sourceScope, 'generated')
assert.equal(root.displayName, 'Hero')
assert.equal(derived.displayName, 'Hero Rigged')
assert.equal(derived.capability, 'rigged-mesh')
assert.equal(derived.createdAt, '2026-06-16T10:00:00.000Z')
assert.equal(derived.semantic?.project, 'Adventure')
assert.deepEqual(derived.semantic?.tags, ['rigged', 'animated', 'clip-hero-walk', 'mid-poly'])
assert.equal(derived.semantic?.derivedFrom?.parent.workspacePath, 'Default/hero.glb')
assert.equal(derived.semantic?.derivedFrom?.root.workspacePath, 'Default/hero.glb')
}))


test('reads and opens only safe GLB/GLTF workspace assets', () => withWorkspace(async (workspaceDir) => {
await mkdir(path.join(workspaceDir, 'Workflows/checkpoints'), { recursive: true })
await mkdir(path.join(workspaceDir, 'Exports'), { recursive: true })
Expand Down Expand Up @@ -210,6 +256,116 @@ test('fails closed for unsafe, self, missing, and mismatched sourceWorkspacePath
assert.equal(!mismatched.success && mismatched.error.code, 'not-openable')
}))

test('reads a sibling .thumb.png for GLB assets and stays silent when one is missing', () => withWorkspace(async (workspaceDir) => {
await mkdir(path.join(workspaceDir, 'Exports/props'), { recursive: true })
await writeFile(path.join(workspaceDir, 'Exports/props/hero.glb'), 'glb')
await writeFile(path.join(workspaceDir, 'Exports/props/hero.thumb.png'), 'fake-png-bytes')
await writeFile(path.join(workspaceDir, 'Exports/props/no-thumb.glb'), 'glb')
await writeFile(path.join(workspaceDir, 'Exports/static.ply'), 'ply')

const withThumb = await readWorkspaceAssetLibraryThumbnail({ workspaceDir, workspacePath: 'Exports/props/hero.glb' })
assert.equal(withThumb.success, true)
assert.equal(withThumb.success && withThumb.dataUrl, `data:image/png;base64,${Buffer.from('fake-png-bytes').toString('base64')}`)

const withoutThumb = await readWorkspaceAssetLibraryThumbnail({ workspaceDir, workspacePath: 'Exports/props/no-thumb.glb' })
assert.equal(withoutThumb.success, false)

const nonMesh = await readWorkspaceAssetLibraryThumbnail({ workspaceDir, workspacePath: 'Exports/static.ply' })
assert.equal(nonMesh.success, false)

const unsafe = await readWorkspaceAssetLibraryThumbnail({ workspaceDir, workspacePath: '../secret.glb' })
assert.equal(unsafe.success, false)
}))

test('attaches preview clip metadata to the thumbnail response when a manifest exists', () => withWorkspace(async (workspaceDir) => {
await mkdir(path.join(workspaceDir, 'Exports/props'), { recursive: true })
await writeFile(path.join(workspaceDir, 'Exports/props/hero.glb'), 'glb')
await writeFile(path.join(workspaceDir, 'Exports/props/hero.thumb.png'), 'fake-png-bytes')
await writeFile(path.join(workspaceDir, 'Exports/props/hero.preview-idle.webp'), 'fake-idle-webp')
await writeFile(path.join(workspaceDir, 'Exports/props/hero.preview-walk.webp'), 'fake-walk-webp')
await writeFile(path.join(workspaceDir, 'Exports/props/hero.previews.json'), JSON.stringify([
{ clip: 'idle', file: 'hero.preview-idle.webp', duration: 1.5 },
{ clip: 'walk', file: 'hero.preview-walk.webp', duration: 0.8 },
]))

const withThumb = await readWorkspaceAssetLibraryThumbnail({ workspaceDir, workspacePath: 'Exports/props/hero.glb' })
assert.equal(withThumb.success, true)
assert.deepEqual(withThumb.success && withThumb.previews, [
{ clip: 'idle', duration: 1.5 },
{ clip: 'walk', duration: 0.8 },
])
}))

test('fetches a specific preview clip WebP by name over the same thumbnail request', () => withWorkspace(async (workspaceDir) => {
await mkdir(path.join(workspaceDir, 'Exports/props'), { recursive: true })
await writeFile(path.join(workspaceDir, 'Exports/props/hero.glb'), 'glb')
await writeFile(path.join(workspaceDir, 'Exports/props/hero.preview-walk.webp'), 'fake-walk-webp')
await writeFile(path.join(workspaceDir, 'Exports/props/hero.previews.json'), JSON.stringify([
{ clip: 'walk', file: 'hero.preview-walk.webp', duration: 0.8 },
]))

const clip = await readWorkspaceAssetLibraryThumbnail({ workspaceDir, workspacePath: 'Exports/props/hero.glb', previewClip: 'walk' })
assert.equal(clip.success, true)
assert.equal(clip.success && clip.dataUrl, `data:image/webp;base64,${Buffer.from('fake-walk-webp').toString('base64')}`)

const unknownClip = await readWorkspaceAssetLibraryThumbnail({ workspaceDir, workspacePath: 'Exports/props/hero.glb', previewClip: 'sprint' })
assert.equal(unknownClip.success, false)
}))

test('thumbnail response carries no previews field when no manifest exists, and stays silent on a malformed one', () => withWorkspace(async (workspaceDir) => {
await mkdir(path.join(workspaceDir, 'Exports/props'), { recursive: true })
await writeFile(path.join(workspaceDir, 'Exports/props/hero.glb'), 'glb')
await writeFile(path.join(workspaceDir, 'Exports/props/hero.thumb.png'), 'fake-png-bytes')
await writeFile(path.join(workspaceDir, 'Exports/props/broken.glb'), 'glb')
await writeFile(path.join(workspaceDir, 'Exports/props/broken.thumb.png'), 'fake-png-bytes')
await writeFile(path.join(workspaceDir, 'Exports/props/broken.previews.json'), 'not valid json{')

const noManifest = await readWorkspaceAssetLibraryThumbnail({ workspaceDir, workspacePath: 'Exports/props/hero.glb' })
assert.equal(noManifest.success, true)
assert.equal(noManifest.success && noManifest.previews, undefined)

const malformedManifest = await readWorkspaceAssetLibraryThumbnail({ workspaceDir, workspacePath: 'Exports/props/broken.glb' })
assert.equal(malformedManifest.success, true)
assert.equal(malformedManifest.success && malformedManifest.previews, undefined)
}))

test('rejects a preview manifest entry that tries to escape the asset directory via its file field', () => withWorkspace(async (workspaceDir) => {
await mkdir(path.join(workspaceDir, 'Exports/props'), { recursive: true })
await writeFile(path.join(workspaceDir, 'Exports/props/hero.glb'), 'glb')
await writeFile(path.join(workspaceDir, 'Exports/props/hero.thumb.png'), 'fake-png-bytes')
await writeFile(path.join(workspaceDir, 'secret.webp'), 'top-secret-bytes')
await writeFile(path.join(workspaceDir, 'Exports/props/hero.previews.json'), JSON.stringify([
{ clip: 'escape-relative', file: '../../secret.webp', duration: 1 },
{ clip: 'escape-absolute', file: '/etc/passwd', duration: 1 },
{ clip: 'wrong-extension', file: 'hero.glb', duration: 1 },
]))

// The manifest lists the clip in its metadata (harmless — it's just a name),
// but every attempt to actually fetch the referenced file must fail closed.
const listed = await readWorkspaceAssetLibraryThumbnail({ workspaceDir, workspacePath: 'Exports/props/hero.glb' })
assert.equal(listed.success, true)
assert.equal(listed.success && listed.previews?.length, 3)

const relative = await readWorkspaceAssetLibraryThumbnail({ workspaceDir, workspacePath: 'Exports/props/hero.glb', previewClip: 'escape-relative' })
assert.equal(relative.success, false)
const absolute = await readWorkspaceAssetLibraryThumbnail({ workspaceDir, workspacePath: 'Exports/props/hero.glb', previewClip: 'escape-absolute' })
assert.equal(absolute.success, false)
const wrongExtension = await readWorkspaceAssetLibraryThumbnail({ workspaceDir, workspacePath: 'Exports/props/hero.glb', previewClip: 'wrong-extension' })
assert.equal(wrongExtension.success, false)
}))

test('IPC thumbnail handler forwards previewClip from payload', async () => {
const handlers = new Map<string, (event: unknown, payload: unknown) => Promise<unknown>>()
registerWorkspaceAssetLibraryIpcHandlers({
ipcMain: { handle: (channel, handler) => handlers.set(channel, handler) },
getWorkspaceDir: () => '/tmp/modly-workspace',
})

const result = await handlers.get('workspace:library:thumbnail')?.({}, { workspacePath: 'Exports/hero.glb', previewClip: 'walk' })
// No such workspace/file in this test, so it must fail closed rather than throw.
assert.equal((result as { success: boolean }).success, false)
})

test('IPC read and open handlers forward sourceWorkspacePath without trusting malformed payloads', async () => {
const handlers = new Map<string, (event: unknown, payload: unknown) => Promise<unknown>>()
registerWorkspaceAssetLibraryIpcHandlers({
Expand All @@ -236,7 +392,12 @@ test('registers workspace library IPC handlers with structured results', async (
assert.equal(typeof handlers.get('workspace:library:list'), 'function')
assert.equal(typeof handlers.get('workspace:library:read'), 'function')
assert.equal(typeof handlers.get('workspace:library:open'), 'function')
assert.equal(typeof handlers.get('workspace:library:thumbnail'), 'function')
const result = await handlers.get('workspace:library:read')?.({}, { workspacePath: '../escape.glb' })
assert.equal(typeof (result as { success?: unknown }).success, 'boolean')
assert.equal((result as { success: boolean, error?: { code: string } }).error?.code, 'unsafe-path')
const thumbnail = await handlers.get('workspace:library:thumbnail')?.({}, { workspacePath: '../escape.glb' })
assert.equal((thumbnail as { success: boolean }).success, false)
const missingPayload = await handlers.get('workspace:library:thumbnail')?.({}, {})
assert.equal((missingPayload as { success: boolean }).success, false)
})
Loading