Initial EveTui runtime
This commit is contained in:
commit
b01f481483
23 changed files with 2159 additions and 0 deletions
48
.github/workflows/runtime.yml
vendored
Normal file
48
.github/workflows/runtime.yml
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
name: EveTui runtime
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-and-pack:
|
||||||
|
runs-on: windows-latest
|
||||||
|
env:
|
||||||
|
GIT_LFS_SKIP_SMUDGE: "1"
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
path: EveTui
|
||||||
|
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
repository: GameCult/Eve
|
||||||
|
path: Eve
|
||||||
|
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
repository: GameCult/Aetheria
|
||||||
|
path: Aetheria
|
||||||
|
lfs: false
|
||||||
|
|
||||||
|
- name: Verify runtime lifecycle
|
||||||
|
working-directory: EveTui
|
||||||
|
shell: powershell
|
||||||
|
run: .\scripts\run-evetui-lifecycle-smoke.ps1
|
||||||
|
|
||||||
|
- name: Pack runtime
|
||||||
|
working-directory: EveTui
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
New-Item -ItemType Directory -Force artifacts\release | Out-Null
|
||||||
|
npm pack --pack-destination artifacts\release
|
||||||
|
|
||||||
|
- name: Upload runtime package
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: eve-tui-${{ github.sha }}
|
||||||
|
path: EveTui/artifacts/release/*.tgz
|
||||||
|
if-no-files-found: error
|
||||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
artifacts/
|
||||||
|
node_modules/
|
||||||
15
README.md
Normal file
15
README.md
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# EveTui
|
||||||
|
|
||||||
|
EveTui is the provider-agnostic compact terminal lowerer for Eve surfaces. It
|
||||||
|
consumes provider advertisements and surface documents, emits typed command
|
||||||
|
intents through provider-owned boundaries, and publishes terminal-grid capture
|
||||||
|
evidence without owning provider state or plugin semantics.
|
||||||
|
|
||||||
|
Eve owns the surface contract. EveTui owns terminal projection, package and test
|
||||||
|
lifecycle, and capture artifacts. Providers remain the only writers of live
|
||||||
|
state.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm test
|
||||||
|
powershell -ExecutionPolicy Bypass -File .\scripts\run-evetui-lifecycle-smoke.ps1
|
||||||
|
```
|
||||||
20
conformance/captures/aetheria/capture-request.json
Normal file
20
conformance/captures/aetheria/capture-request.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"schema": "gamecult.eve.runtime_capture_request.v1",
|
||||||
|
"runtimeId": "tui",
|
||||||
|
"ownerRepo": "EveTui",
|
||||||
|
"providerId": "aetheria",
|
||||||
|
"surfaceId": "aetheria.daemon.game",
|
||||||
|
"surfaceKind": "interactive-world",
|
||||||
|
"targetId": "tui",
|
||||||
|
"projectionKind": "provider-authored-world-surface",
|
||||||
|
"commandBoundary": "aetheria.daemon.commands",
|
||||||
|
"receiptSchema": "aetheria.eve_command_acceptance_status.v1",
|
||||||
|
"captureKind": "terminal-cell-grid",
|
||||||
|
"artifactKind": "json-grid",
|
||||||
|
"artifactPath": "artifacts/evetui-capture/latest/tui-grid.json",
|
||||||
|
"conformanceAttachment": "runtime.captureArtifacts[]",
|
||||||
|
"authority": "Capture observes terminal projection only; provider state, command acceptance, and receipts stay provider-owned.",
|
||||||
|
"sourceAdvertisementPath": "E:\\Projects\\Eve\\..\\Aetheria\\conformance\\eve\\aetheria.provider-advertisement.json",
|
||||||
|
"sourceCapabilityManifestPath": "E:\\Projects\\Eve\\runtimes\\incubating\\eve-tui\\eve-runtime-capability.json",
|
||||||
|
"issuedAtUtc": "latest"
|
||||||
|
}
|
||||||
40
conformance/captures/aetheria/tui-grid.json
Normal file
40
conformance/captures/aetheria/tui-grid.json
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
"schema": "gamecult.eve.tui_grid.v1",
|
||||||
|
"runtimeId": "tui",
|
||||||
|
"providerId": "aetheria",
|
||||||
|
"surfaceId": "aetheria.daemon.game",
|
||||||
|
"projectionKind": "provider-authored-world-surface",
|
||||||
|
"commandBoundary": "aetheria.daemon.commands",
|
||||||
|
"receiptSchema": "aetheria.eve_command_acceptance_status.v1",
|
||||||
|
"ownership": "provider-owns-world-state-assets-command-acceptance-and-receipts",
|
||||||
|
"width": 80,
|
||||||
|
"lines": [
|
||||||
|
"EveTui aetheria",
|
||||||
|
"surface aetheria.daemon.game",
|
||||||
|
"command aetheria.daemon.commands",
|
||||||
|
"receipt aetheria.eve_command_acceptance_status.v1",
|
||||||
|
"surface aetheria.daemon.game.root",
|
||||||
|
" card aetheria.daemon.game.summary Daemon World",
|
||||||
|
" text aetheria.daemon.game.summary.text Provider-owned world state project...",
|
||||||
|
" metric aetheria.daemon.game.frame Frame",
|
||||||
|
" partition aetheria.daemon.game.partition",
|
||||||
|
" world aetheria.daemon.game.playable Daemon ARPG View",
|
||||||
|
" world aetheria.daemon.game.entity.player Vanguard",
|
||||||
|
" world aetheria.daemon.game.entity.raider Raider Scout",
|
||||||
|
" world aetheria.daemon.game.entity.station Anchor Station",
|
||||||
|
" world aetheria.daemon.game.flow Volumetric Flow",
|
||||||
|
" world aetheria.daemon.game.gravity Current Zone Gravity",
|
||||||
|
" card aetheria.daemon.game.commands Command Boundary",
|
||||||
|
" command aetheria.daemon.game.command.focus Focus Relay",
|
||||||
|
" command aetheria.daemon.game.command.move Issue Move Intent",
|
||||||
|
" text aetheria.daemon.game.authority Commands are intents against Aether..."
|
||||||
|
],
|
||||||
|
"embeddedDocumentSlots": [],
|
||||||
|
"pluginProjections": [],
|
||||||
|
"lossiness": "terminal-grid-command-surface; summarizes provider-authored world surface without owning provider state",
|
||||||
|
"artifactKind": "json-grid",
|
||||||
|
"captureKind": "terminal-cell-grid",
|
||||||
|
"sourceAdvertisementPath": "E:\\Projects\\Eve\\..\\Aetheria\\conformance\\eve\\aetheria.provider-advertisement.json",
|
||||||
|
"sourceCapabilityManifestPath": "E:\\Projects\\Eve\\runtimes\\incubating\\eve-tui\\eve-runtime-capability.json",
|
||||||
|
"issuedAtUtc": "latest"
|
||||||
|
}
|
||||||
20
conformance/captures/world-smoke/capture-request.json
Normal file
20
conformance/captures/world-smoke/capture-request.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"schema": "gamecult.eve.runtime_capture_request.v1",
|
||||||
|
"runtimeId": "tui",
|
||||||
|
"ownerRepo": "EveTui",
|
||||||
|
"providerId": "eve.world-smoke",
|
||||||
|
"surfaceId": "eve.world-smoke.surface",
|
||||||
|
"surfaceKind": "interactive-world",
|
||||||
|
"targetId": "tui",
|
||||||
|
"projectionKind": "provider-authored-world-surface",
|
||||||
|
"commandBoundary": "eve.world-smoke.commands",
|
||||||
|
"receiptSchema": "eve.world_smoke.command_receipt.v1",
|
||||||
|
"captureKind": "terminal-cell-grid",
|
||||||
|
"artifactKind": "json-grid",
|
||||||
|
"artifactPath": "artifacts/evetui-capture/latest/tui-grid.json",
|
||||||
|
"conformanceAttachment": "runtime.captureArtifacts[]",
|
||||||
|
"authority": "Capture observes terminal projection only; provider state, command acceptance, and receipts stay provider-owned.",
|
||||||
|
"sourceAdvertisementPath": "E:\\Projects\\Eve\\web\\fixtures\\eve-world-smoke.provider-advertisement.json",
|
||||||
|
"sourceCapabilityManifestPath": "E:\\Projects\\Eve\\runtimes\\incubating\\eve-tui\\eve-runtime-capability.json",
|
||||||
|
"issuedAtUtc": "latest"
|
||||||
|
}
|
||||||
33
conformance/captures/world-smoke/tui-grid.json
Normal file
33
conformance/captures/world-smoke/tui-grid.json
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"schema": "gamecult.eve.tui_grid.v1",
|
||||||
|
"runtimeId": "tui",
|
||||||
|
"providerId": "eve.world-smoke",
|
||||||
|
"surfaceId": "eve.world-smoke.surface",
|
||||||
|
"projectionKind": "provider-authored-world-surface",
|
||||||
|
"commandBoundary": "eve.world-smoke.commands",
|
||||||
|
"receiptSchema": "eve.world_smoke.command_receipt.v1",
|
||||||
|
"ownership": "provider-owns-world-state-and-command-receipts",
|
||||||
|
"width": 80,
|
||||||
|
"lines": [
|
||||||
|
"EveTui eve.world-smoke",
|
||||||
|
"surface eve.world-smoke.surface",
|
||||||
|
"command eve.world-smoke.commands",
|
||||||
|
"receipt eve.world_smoke.command_receipt.v1",
|
||||||
|
"surface eve.world-smoke.root",
|
||||||
|
" card eve.world-smoke.summary World Contract Control",
|
||||||
|
" text eve.world-smoke.summary.text A tiny provider-owned world surface use...",
|
||||||
|
" metric eve.world-smoke.tick Tick",
|
||||||
|
" world eve.world-smoke.field Contract Field",
|
||||||
|
" card eve.world-smoke.commands Command Boundary",
|
||||||
|
" command eve.world-smoke.command.focus Focus Beacon",
|
||||||
|
" text eve.world-smoke.authority The provider owns accepted state and recei..."
|
||||||
|
],
|
||||||
|
"embeddedDocumentSlots": [],
|
||||||
|
"pluginProjections": [],
|
||||||
|
"lossiness": "terminal-grid-command-surface; summarizes provider-authored world surface without owning provider state",
|
||||||
|
"artifactKind": "json-grid",
|
||||||
|
"captureKind": "terminal-cell-grid",
|
||||||
|
"sourceAdvertisementPath": "E:\\Projects\\Eve\\web\\fixtures\\eve-world-smoke.provider-advertisement.json",
|
||||||
|
"sourceCapabilityManifestPath": "E:\\Projects\\Eve\\runtimes\\incubating\\eve-tui\\eve-runtime-capability.json",
|
||||||
|
"issuedAtUtc": "latest"
|
||||||
|
}
|
||||||
195
eve-runtime-capability.json
Normal file
195
eve-runtime-capability.json
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
{
|
||||||
|
"schema": "gamecult.eve.runtime_capability.v1",
|
||||||
|
"runtimeId": "tui",
|
||||||
|
"title": "Compact TUI",
|
||||||
|
"owner": "EveTui",
|
||||||
|
"incubation": {
|
||||||
|
"status": "graduated",
|
||||||
|
"ownerRepo": "EveTui",
|
||||||
|
"currentHostRepo": "EveTui",
|
||||||
|
"splitTarget": "EveTui",
|
||||||
|
"graduationTrigger": "Graduated: EveTui owns package, tests, terminal lowering, command transport, lifecycle, and capture evidence."
|
||||||
|
},
|
||||||
|
"supportedFeatures": [
|
||||||
|
"providerAdvertisements",
|
||||||
|
"commandTransport",
|
||||||
|
"terminalGridSummary",
|
||||||
|
"terminalGridLowering",
|
||||||
|
"embeddedDocuments",
|
||||||
|
"pluginProjection"
|
||||||
|
],
|
||||||
|
"supportedPlugins": [
|
||||||
|
{
|
||||||
|
"pluginId": "sai.vn",
|
||||||
|
"capabilities": [
|
||||||
|
"vn.stage",
|
||||||
|
"story.choose",
|
||||||
|
"story.continue",
|
||||||
|
"story.jump"
|
||||||
|
],
|
||||||
|
"projectionAdapter": "EveTuiShell.buildPluginProjection",
|
||||||
|
"lossiness": "terminal-summary"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pluginId": "norn.graph",
|
||||||
|
"capabilities": [
|
||||||
|
"embed.norn"
|
||||||
|
],
|
||||||
|
"projectionAdapter": "EveTuiShell.buildPluginProjection",
|
||||||
|
"lossiness": "terminal-outline"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pluginId": "tex.math",
|
||||||
|
"capabilities": [
|
||||||
|
"embed.tex",
|
||||||
|
"tex.inline",
|
||||||
|
"tex.block"
|
||||||
|
],
|
||||||
|
"projectionAdapter": "EveTuiShell.buildPluginProjection",
|
||||||
|
"lossiness": "source-text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unsupportedPlugins": [],
|
||||||
|
"worldSurfaceLowering": [
|
||||||
|
{
|
||||||
|
"targetId": "tui",
|
||||||
|
"surfaceKinds": ["interactive-world", "interactive-world-editor"],
|
||||||
|
"projectionKinds": ["provider-authored-world-surface", "provider-authored-world-editor-surface"],
|
||||||
|
"supportLevel": "terminal-grid-command-surface",
|
||||||
|
"ownership": "runtime-lowers-provider-world-surface-without-owning-world-state",
|
||||||
|
"lossiness": "terminal-grid-summary-only",
|
||||||
|
"evidencePaths": [
|
||||||
|
"src/eve-tui-shell.mjs",
|
||||||
|
"test/eve-tui-shell.test.mjs",
|
||||||
|
"../Eve/web/fixtures/sai-vn-surface.json",
|
||||||
|
"../Aetheria/conformance/eve/aetheria.provider-advertisement.json",
|
||||||
|
"../Eve/web/fixtures/eve-world-smoke.provider-advertisement.json"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"commandTransport": {
|
||||||
|
"schema": "gamecult.eve.command_invocation.v1",
|
||||||
|
"status": "provider-shell-contract-skeleton",
|
||||||
|
"emitter": "EveTuiShell.createCommandIntent",
|
||||||
|
"clientId": "tui",
|
||||||
|
"authority": "TUI emits command intents through provider-advertised command boundaries; providers accept, deny, and publish receipts."
|
||||||
|
},
|
||||||
|
"lifecycle": {
|
||||||
|
"release": {
|
||||||
|
"ownerRepo": "EveTui",
|
||||||
|
"status": "owner-npm-package",
|
||||||
|
"releaseContract": {
|
||||||
|
"ownerRepo": "EveTui",
|
||||||
|
"packageName": "eve-tui",
|
||||||
|
"packageRoot": ".",
|
||||||
|
"versionSource": "package.json",
|
||||||
|
"tagPattern": "evetui-v{version}",
|
||||||
|
"artifactKind": "terminal-runtime",
|
||||||
|
"requestSchema": "gamecult.eve.runtime_release_request.v1",
|
||||||
|
"requestBuilder": "tools/evetui/evetui-release-contract.mjs",
|
||||||
|
"artifactPattern": "artifacts/evetui-release/{version}/eve-tui-{version}.tgz",
|
||||||
|
"publishProof": "Terminal runtime release is created from the EveTui repo and references the provider-agnostic TUI package root."
|
||||||
|
},
|
||||||
|
"evidencePaths": [
|
||||||
|
"package.json",
|
||||||
|
"src/eve-tui-shell.mjs",
|
||||||
|
"tools/evetui/evetui-release-contract.mjs",
|
||||||
|
"tools/evetui/evetui-release-contract.test.mjs",
|
||||||
|
"scripts/run-evetui-release-contract-smoke.ps1"
|
||||||
|
],
|
||||||
|
"pendingProofs": []
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"ownerRepo": "EveTui",
|
||||||
|
"status": "provider-shell-contract-skeleton",
|
||||||
|
"testContract": {
|
||||||
|
"ownerRepo": "EveTui",
|
||||||
|
"runnerKind": "terminal-provider-shell-smoke",
|
||||||
|
"runnerScript": "scripts/run-evetui-provider-shell-smoke.ps1",
|
||||||
|
"consumerProject": "../Aetheria",
|
||||||
|
"packageName": "eve-tui",
|
||||||
|
"testAssembly": "eve-tui-shell.test.mjs",
|
||||||
|
"testPlatform": "Node terminal-grid contract skeleton",
|
||||||
|
"resultsArtifact": "artifacts/evetui-provider-shell/{stamp}/provider-shell-results.json",
|
||||||
|
"logArtifact": "artifacts/evetui-provider-shell/{stamp}/provider-shell.log",
|
||||||
|
"manifestMutation": "None; the TUI consumes provider advertisements and must not mutate provider product manifests."
|
||||||
|
},
|
||||||
|
"evidencePaths": [
|
||||||
|
"package.json",
|
||||||
|
"src/eve-tui-shell.mjs",
|
||||||
|
"test/eve-tui-shell.test.mjs",
|
||||||
|
"scripts/run-evetui-provider-shell-smoke.ps1",
|
||||||
|
"conformance/captures/aetheria/tui-grid.json",
|
||||||
|
"conformance/captures/world-smoke/tui-grid.json"
|
||||||
|
],
|
||||||
|
"pendingProofs": []
|
||||||
|
},
|
||||||
|
"capture": {
|
||||||
|
"ownerRepo": "EveTui",
|
||||||
|
"status": "cell-grid-json-capture",
|
||||||
|
"captureContract": {
|
||||||
|
"ownerRepo": "EveTui",
|
||||||
|
"runtimeId": "tui",
|
||||||
|
"targetId": "tui",
|
||||||
|
"requestSchema": "gamecult.eve.runtime_capture_request.v1",
|
||||||
|
"requestBuilder": "tools/evetui/evetui-capture-contract.mjs",
|
||||||
|
"advertisementPath": "../Aetheria/conformance/eve/aetheria.provider-advertisement.json",
|
||||||
|
"captureKind": "terminal-cell-grid",
|
||||||
|
"artifactKind": "json-grid",
|
||||||
|
"artifactPattern": "artifacts/evetui-capture/{stamp}/tui-grid.json",
|
||||||
|
"conformanceAttachment": "runtime.captureArtifacts[]",
|
||||||
|
"requiredProvider": "aetheria",
|
||||||
|
"requiredSurface": "aetheria.daemon.game",
|
||||||
|
"additionalProviderSurfaces": [
|
||||||
|
{
|
||||||
|
"providerId": "eve.world-smoke",
|
||||||
|
"surfaceId": "eve.world-smoke.surface",
|
||||||
|
"purpose": "Generic non-product interactive-world capture control."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"providerId": "aetheria",
|
||||||
|
"surfaceId": "aetheria.daemon.editor",
|
||||||
|
"purpose": "Aetheria interactive-world-editor capture control."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"authority": "Capture observes terminal projection only; provider state, command acceptance, and receipts stay provider-owned.",
|
||||||
|
"publishProof": "Terminal cell-grid JSON capture artifact is attached to the runtime conformance report from EveTui."
|
||||||
|
},
|
||||||
|
"evidencePaths": [
|
||||||
|
"tools/evetui/evetui-capture-contract.mjs",
|
||||||
|
"tools/evetui/evetui-capture-contract.test.mjs",
|
||||||
|
"tools/evetui/evetui-capture-artifact.mjs",
|
||||||
|
"tools/evetui/evetui-capture-artifact.test.mjs",
|
||||||
|
"scripts/run-evetui-capture-contract-smoke.ps1",
|
||||||
|
"scripts/run-evetui-capture-smoke.ps1",
|
||||||
|
"conformance/captures/aetheria/tui-grid.json",
|
||||||
|
"conformance/captures/world-smoke/tui-grid.json"
|
||||||
|
],
|
||||||
|
"pendingProofs": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"capture": {
|
||||||
|
"status": "json-grid",
|
||||||
|
"note": "EveTui publishes provider-advertisement-backed gamecult.eve.tui_grid.v1 release captures.",
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"kind": "json-grid",
|
||||||
|
"schema": "gamecult.eve.tui_grid.v1",
|
||||||
|
"path": "conformance/captures/aetheria/tui-grid.json",
|
||||||
|
"providerId": "aetheria",
|
||||||
|
"surfaceId": "aetheria.daemon.game",
|
||||||
|
"sourceSurfacePath": "../Aetheria/conformance/eve/aetheria-world-surface.json",
|
||||||
|
"requestPath": "conformance/captures/aetheria/capture-request.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "json-grid",
|
||||||
|
"schema": "gamecult.eve.tui_grid.v1",
|
||||||
|
"path": "conformance/captures/world-smoke/tui-grid.json",
|
||||||
|
"providerId": "eve.world-smoke",
|
||||||
|
"surfaceId": "eve.world-smoke.surface",
|
||||||
|
"sourceSurfacePath": "../Eve/web/fixtures/eve-world-smoke-surface.json",
|
||||||
|
"requestPath": "conformance/captures/world-smoke/capture-request.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
18
package.json
Normal file
18
package.json
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"name": "eve-tui",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"description": "Provider-agnostic compact TUI shell skeleton for Eve surfaces.",
|
||||||
|
"main": "src/eve-tui-shell.mjs",
|
||||||
|
"scripts": {
|
||||||
|
"test": "C:\\Progra~1\\nodejs\\node.exe --test test/*.test.mjs",
|
||||||
|
"test:all": "C:\\Progra~1\\nodejs\\node.exe --test test/*.test.mjs tools/evetui/*.test.mjs"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"Eve",
|
||||||
|
"TUI",
|
||||||
|
"CultUI",
|
||||||
|
"provider-advertisement"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0"
|
||||||
|
}
|
||||||
99
scripts/run-evetui-capture-contract-smoke.ps1
Normal file
99
scripts/run-evetui-capture-contract-smoke.ps1
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
param(
|
||||||
|
[string] $CapabilityManifestPath = "eve-runtime-capability.json",
|
||||||
|
[string] $AdvertisementPath = "..\Aetheria\conformance\eve\aetheria.provider-advertisement.json",
|
||||||
|
[string] $OutputPath = "artifacts\evetui-capture-contract\latest\capture-request.json",
|
||||||
|
[string] $Stamp = "smoke"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$projectRoot = Split-Path -Parent $PSScriptRoot
|
||||||
|
$absoluteCapabilityPath = if ([System.IO.Path]::IsPathRooted($CapabilityManifestPath)) {
|
||||||
|
$CapabilityManifestPath
|
||||||
|
} else {
|
||||||
|
Join-Path $projectRoot $CapabilityManifestPath
|
||||||
|
}
|
||||||
|
$absoluteAdvertisementPath = if ([System.IO.Path]::IsPathRooted($AdvertisementPath)) {
|
||||||
|
$AdvertisementPath
|
||||||
|
} else {
|
||||||
|
Join-Path $projectRoot $AdvertisementPath
|
||||||
|
}
|
||||||
|
$absoluteOutputPath = if ([System.IO.Path]::IsPathRooted($OutputPath)) {
|
||||||
|
$OutputPath
|
||||||
|
} else {
|
||||||
|
Join-Path $projectRoot $OutputPath
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($path in @($absoluteCapabilityPath, $absoluteAdvertisementPath)) {
|
||||||
|
if (-not (Test-Path -LiteralPath $path)) {
|
||||||
|
throw "EveTui capture contract input not found: $path"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$manifest = Get-Content -LiteralPath $absoluteCapabilityPath -Raw | ConvertFrom-Json
|
||||||
|
$captureContract = $manifest.lifecycle.capture.captureContract
|
||||||
|
if ($null -eq $captureContract) {
|
||||||
|
throw "EveTui capture contract missing lifecycle.capture.captureContract"
|
||||||
|
}
|
||||||
|
foreach ($field in @("ownerRepo", "runtimeId", "targetId", "requestSchema", "requestBuilder", "advertisementPath", "captureKind", "artifactKind", "artifactPattern", "conformanceAttachment", "requiredProvider", "requiredSurface", "authority", "publishProof")) {
|
||||||
|
if (-not $captureContract.$field) {
|
||||||
|
throw "EveTui capture contract missing $field"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($captureContract.ownerRepo -ne "EveTui") {
|
||||||
|
throw "Unexpected EveTui capture owner: $($captureContract.ownerRepo)"
|
||||||
|
}
|
||||||
|
if ($captureContract.runtimeId -ne "tui") {
|
||||||
|
throw "Unexpected EveTui capture runtime: $($captureContract.runtimeId)"
|
||||||
|
}
|
||||||
|
if ($captureContract.targetId -ne "tui") {
|
||||||
|
throw "Unexpected EveTui capture target: $($captureContract.targetId)"
|
||||||
|
}
|
||||||
|
if ($captureContract.requestSchema -ne "gamecult.eve.runtime_capture_request.v1") {
|
||||||
|
throw "Unexpected EveTui capture request schema: $($captureContract.requestSchema)"
|
||||||
|
}
|
||||||
|
|
||||||
|
$builderPath = Join-Path $projectRoot $captureContract.requestBuilder
|
||||||
|
if (-not (Test-Path -LiteralPath $builderPath)) {
|
||||||
|
throw "EveTui capture request builder not found: $($captureContract.requestBuilder)"
|
||||||
|
}
|
||||||
|
if ((Join-Path $projectRoot $captureContract.advertisementPath) -ne $absoluteAdvertisementPath) {
|
||||||
|
throw "EveTui capture advertisement path mismatch: $($captureContract.advertisementPath) vs $AdvertisementPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
node --test (Join-Path $projectRoot "tools\evetui\evetui-capture-contract.test.mjs")
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "EveTui capture contract tests failed with exit code $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
|
||||||
|
node $builderPath --capability $absoluteCapabilityPath --advertisement $absoluteAdvertisementPath --output $absoluteOutputPath --stamp $Stamp
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "EveTui capture request builder failed with exit code $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
if (-not (Test-Path -LiteralPath $absoluteOutputPath)) {
|
||||||
|
throw "EveTui capture request was not written: $absoluteOutputPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
$request = Get-Content -LiteralPath $absoluteOutputPath -Raw | ConvertFrom-Json
|
||||||
|
foreach ($expectation in @(
|
||||||
|
@{ field = "schema"; value = "gamecult.eve.runtime_capture_request.v1" },
|
||||||
|
@{ field = "runtimeId"; value = "tui" },
|
||||||
|
@{ field = "ownerRepo"; value = "EveTui" },
|
||||||
|
@{ field = "providerId"; value = "aetheria" },
|
||||||
|
@{ field = "surfaceId"; value = "aetheria.daemon.game" },
|
||||||
|
@{ field = "targetId"; value = "tui" },
|
||||||
|
@{ field = "commandBoundary"; value = "aetheria.daemon.commands" },
|
||||||
|
@{ field = "receiptSchema"; value = "aetheria.eve_command_acceptance_status.v1" },
|
||||||
|
@{ field = "captureKind"; value = "terminal-cell-grid" },
|
||||||
|
@{ field = "artifactKind"; value = "json-grid" },
|
||||||
|
@{ field = "conformanceAttachment"; value = "runtime.captureArtifacts[]" }
|
||||||
|
)) {
|
||||||
|
if ($request.($expectation.field) -ne $expectation.value) {
|
||||||
|
throw "EveTui capture request $($expectation.field) expected $($expectation.value) got $($request.($expectation.field))"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($request.artifactPath -ne "artifacts/evetui-capture/$Stamp/tui-grid.json") {
|
||||||
|
throw "Unexpected EveTui capture artifact path: $($request.artifactPath)"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "EveTui capture contract smoke passed: $absoluteOutputPath"
|
||||||
77
scripts/run-evetui-capture-smoke.ps1
Normal file
77
scripts/run-evetui-capture-smoke.ps1
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
param(
|
||||||
|
[string] $CapabilityManifestPath = "eve-runtime-capability.json",
|
||||||
|
[string] $AdvertisementPath = "..\Aetheria\conformance\eve\aetheria.provider-advertisement.json",
|
||||||
|
[string] $SurfacePath = "..\Aetheria\conformance\eve\aetheria-world-surface.json",
|
||||||
|
[string] $OutputPath = "artifacts\evetui-capture\latest\tui-grid.json",
|
||||||
|
[string] $RequestOutputPath = "artifacts\evetui-capture\latest\capture-request.json",
|
||||||
|
[string] $Stamp = "latest",
|
||||||
|
[string] $ExpectedProviderId = "aetheria",
|
||||||
|
[string] $ExpectedSurfaceId = "aetheria.daemon.game",
|
||||||
|
[string] $ExpectedCommandBoundary = "aetheria.daemon.commands",
|
||||||
|
[string] $ExpectedReceiptSchema = "aetheria.eve_command_acceptance_status.v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$projectRoot = Split-Path -Parent $PSScriptRoot
|
||||||
|
function Resolve-RepoPath([string] $candidate) {
|
||||||
|
if ([System.IO.Path]::IsPathRooted($candidate)) {
|
||||||
|
return $candidate
|
||||||
|
}
|
||||||
|
return Join-Path $projectRoot $candidate
|
||||||
|
}
|
||||||
|
|
||||||
|
$absoluteCapabilityPath = Resolve-RepoPath $CapabilityManifestPath
|
||||||
|
$absoluteAdvertisementPath = Resolve-RepoPath $AdvertisementPath
|
||||||
|
$absoluteSurfacePath = Resolve-RepoPath $SurfacePath
|
||||||
|
$absoluteOutputPath = Resolve-RepoPath $OutputPath
|
||||||
|
$absoluteRequestOutputPath = Resolve-RepoPath $RequestOutputPath
|
||||||
|
|
||||||
|
foreach ($path in @($absoluteCapabilityPath, $absoluteAdvertisementPath, $absoluteSurfacePath)) {
|
||||||
|
if (-not (Test-Path -LiteralPath $path)) {
|
||||||
|
throw "EveTui capture input not found: $path"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node --test (Join-Path $projectRoot "tools\evetui\evetui-capture-artifact.test.mjs")
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "EveTui capture artifact tests failed with exit code $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
|
||||||
|
node (Join-Path $projectRoot "tools\evetui\evetui-capture-artifact.mjs") `
|
||||||
|
--capability $absoluteCapabilityPath `
|
||||||
|
--advertisement $absoluteAdvertisementPath `
|
||||||
|
--surface $absoluteSurfacePath `
|
||||||
|
--output $absoluteOutputPath `
|
||||||
|
--request-output $absoluteRequestOutputPath `
|
||||||
|
--stamp $Stamp
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "EveTui capture artifact builder failed with exit code $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($path in @($absoluteOutputPath, $absoluteRequestOutputPath)) {
|
||||||
|
if (-not (Test-Path -LiteralPath $path)) {
|
||||||
|
throw "EveTui capture artifact output missing: $path"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$grid = Get-Content -LiteralPath $absoluteOutputPath -Raw | ConvertFrom-Json
|
||||||
|
foreach ($expectation in @(
|
||||||
|
@{ field = "schema"; value = "gamecult.eve.tui_grid.v1" },
|
||||||
|
@{ field = "runtimeId"; value = "tui" },
|
||||||
|
@{ field = "providerId"; value = $ExpectedProviderId },
|
||||||
|
@{ field = "surfaceId"; value = $ExpectedSurfaceId },
|
||||||
|
@{ field = "commandBoundary"; value = $ExpectedCommandBoundary },
|
||||||
|
@{ field = "receiptSchema"; value = $ExpectedReceiptSchema },
|
||||||
|
@{ field = "artifactKind"; value = "json-grid" },
|
||||||
|
@{ field = "captureKind"; value = "terminal-cell-grid" }
|
||||||
|
)) {
|
||||||
|
if ($grid.($expectation.field) -ne $expectation.value) {
|
||||||
|
throw "EveTui grid $($expectation.field) expected $($expectation.value) got $($grid.($expectation.field))"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (@($grid.lines).Count -lt 1) {
|
||||||
|
throw "EveTui grid capture must contain terminal lines"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "EveTui capture smoke passed: $absoluteOutputPath"
|
||||||
195
scripts/run-evetui-lifecycle-smoke.ps1
Normal file
195
scripts/run-evetui-lifecycle-smoke.ps1
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
param(
|
||||||
|
[string] $CapabilityManifestPath = "eve-runtime-capability.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$projectRoot = Split-Path -Parent $PSScriptRoot
|
||||||
|
$absoluteManifestPath = if ([System.IO.Path]::IsPathRooted($CapabilityManifestPath)) {
|
||||||
|
$CapabilityManifestPath
|
||||||
|
} else {
|
||||||
|
Join-Path $projectRoot $CapabilityManifestPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $absoluteManifestPath)) {
|
||||||
|
throw "EveTui runtime capability manifest not found: $absoluteManifestPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
$manifest = Get-Content -LiteralPath $absoluteManifestPath -Raw | ConvertFrom-Json
|
||||||
|
|
||||||
|
if ($manifest.schema -ne "gamecult.eve.runtime_capability.v1") {
|
||||||
|
throw "Unexpected EveTui manifest schema: $($manifest.schema)"
|
||||||
|
}
|
||||||
|
if ($manifest.runtimeId -ne "tui") {
|
||||||
|
throw "Unexpected EveTui runtime id: $($manifest.runtimeId)"
|
||||||
|
}
|
||||||
|
if ($manifest.owner -ne "EveTui") {
|
||||||
|
throw "Unexpected EveTui owner: $($manifest.owner)"
|
||||||
|
}
|
||||||
|
if ($manifest.incubation.ownerRepo -ne "EveTui") {
|
||||||
|
throw "Unexpected EveTui incubation owner: $($manifest.incubation.ownerRepo)"
|
||||||
|
}
|
||||||
|
if ($manifest.incubation.currentHostRepo -ne "EveTui") {
|
||||||
|
throw "Unexpected EveTui current host repo: $($manifest.incubation.currentHostRepo)"
|
||||||
|
}
|
||||||
|
if ($manifest.incubation.splitTarget -ne "EveTui") {
|
||||||
|
throw "Unexpected EveTui split target: $($manifest.incubation.splitTarget)"
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($feature in @("providerAdvertisements", "commandTransport", "terminalGridSummary", "terminalGridLowering", "embeddedDocuments", "pluginProjection")) {
|
||||||
|
if (-not (@($manifest.supportedFeatures) -contains $feature)) {
|
||||||
|
throw "EveTui manifest missing provider-shell feature: $feature"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$expectedPlugins = @{
|
||||||
|
"sai.vn" = @("vn.stage", "story.choose", "story.continue", "story.jump")
|
||||||
|
"norn.graph" = @("embed.norn")
|
||||||
|
"tex.math" = @("embed.tex", "tex.inline", "tex.block")
|
||||||
|
}
|
||||||
|
foreach ($pluginId in $expectedPlugins.Keys) {
|
||||||
|
$supported = @($manifest.supportedPlugins) | Where-Object { $_.pluginId -eq $pluginId } | Select-Object -First 1
|
||||||
|
if (-not $supported) {
|
||||||
|
throw "EveTui manifest missing supported plugin declaration: $pluginId"
|
||||||
|
}
|
||||||
|
foreach ($capability in $expectedPlugins[$pluginId]) {
|
||||||
|
if (-not (@($supported.capabilities) -contains $capability)) {
|
||||||
|
throw "EveTui supported plugin $pluginId missing capability: $capability"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($supported.projectionAdapter -ne "EveTuiShell.buildPluginProjection") {
|
||||||
|
throw "EveTui supported plugin $pluginId has unexpected projection adapter: $($supported.projectionAdapter)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$worldSurfaceLoweringClaims = @()
|
||||||
|
if ($null -ne $manifest.worldSurfaceLowering) {
|
||||||
|
$worldSurfaceLoweringClaims = @($manifest.worldSurfaceLowering)
|
||||||
|
}
|
||||||
|
if ($worldSurfaceLoweringClaims.Count -ne 1) {
|
||||||
|
throw "EveTui must claim exactly one world-surface lowering target"
|
||||||
|
}
|
||||||
|
$worldSurfaceLoweringClaim = $worldSurfaceLoweringClaims | Where-Object { $_.targetId -eq "tui" } | Select-Object -First 1
|
||||||
|
if (-not $worldSurfaceLoweringClaim) {
|
||||||
|
throw "EveTui manifest missing tui world-surface lowering claim"
|
||||||
|
}
|
||||||
|
if ($worldSurfaceLoweringClaim.supportLevel -ne "terminal-grid-command-surface") {
|
||||||
|
throw "Unexpected EveTui world-surface support level: $($worldSurfaceLoweringClaim.supportLevel)"
|
||||||
|
}
|
||||||
|
if ($worldSurfaceLoweringClaim.ownership -ne "runtime-lowers-provider-world-surface-without-owning-world-state") {
|
||||||
|
throw "Unexpected EveTui world-surface ownership: $($worldSurfaceLoweringClaim.ownership)"
|
||||||
|
}
|
||||||
|
foreach ($evidencePath in @($worldSurfaceLoweringClaim.evidencePaths)) {
|
||||||
|
$absoluteEvidencePath = Join-Path $projectRoot $evidencePath
|
||||||
|
if (-not (Test-Path -LiteralPath $absoluteEvidencePath)) {
|
||||||
|
throw "EveTui world-surface claim missing evidence path: $evidencePath"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($pluginId in $expectedPlugins.Keys) {
|
||||||
|
$unsupported = @($manifest.unsupportedPlugins) | Where-Object { $_.pluginId -eq $pluginId } | Select-Object -First 1
|
||||||
|
if ($unsupported) {
|
||||||
|
throw "EveTui manifest must not report $pluginId unsupported while terminal fallback projection is declared"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($manifest.commandTransport.schema -ne "gamecult.eve.command_invocation.v1") {
|
||||||
|
throw "Unexpected EveTui command schema: $($manifest.commandTransport.schema)"
|
||||||
|
}
|
||||||
|
if ($manifest.commandTransport.status -ne "provider-shell-contract-skeleton") {
|
||||||
|
throw "Unexpected EveTui command transport status: $($manifest.commandTransport.status)"
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($stage in @("release", "test", "capture")) {
|
||||||
|
$stageDocument = $manifest.lifecycle.$stage
|
||||||
|
if ($null -eq $stageDocument) {
|
||||||
|
throw "EveTui lifecycle missing stage: $stage"
|
||||||
|
}
|
||||||
|
if ($stageDocument.ownerRepo -ne "EveTui") {
|
||||||
|
throw "EveTui lifecycle stage $stage has unexpected owner: $($stageDocument.ownerRepo)"
|
||||||
|
}
|
||||||
|
if (-not $stageDocument.status) {
|
||||||
|
throw "EveTui lifecycle stage $stage missing status"
|
||||||
|
}
|
||||||
|
foreach ($evidencePath in @($stageDocument.evidencePaths)) {
|
||||||
|
$absoluteEvidencePath = Join-Path $projectRoot $evidencePath
|
||||||
|
if (-not (Test-Path -LiteralPath $absoluteEvidencePath)) {
|
||||||
|
throw "EveTui lifecycle stage $stage missing evidence path: $evidencePath"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$releaseContract = $manifest.lifecycle.release.releaseContract
|
||||||
|
foreach ($field in @("ownerRepo", "packageName", "packageRoot", "versionSource", "tagPattern", "artifactKind", "requestSchema", "requestBuilder", "artifactPattern", "publishProof")) {
|
||||||
|
if (-not $releaseContract.$field) {
|
||||||
|
throw "EveTui release contract missing $field"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($releaseContract.requestSchema -ne "gamecult.eve.runtime_release_request.v1") {
|
||||||
|
throw "EveTui release contract has unexpected request schema: $($releaseContract.requestSchema)"
|
||||||
|
}
|
||||||
|
foreach ($pathProperty in @("packageRoot", "versionSource")) {
|
||||||
|
$relativePath = $releaseContract.$pathProperty
|
||||||
|
$absolutePath = Join-Path $projectRoot $relativePath
|
||||||
|
if (-not (Test-Path -LiteralPath $absolutePath)) {
|
||||||
|
throw "EveTui release contract $pathProperty does not exist: $relativePath"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($pathProperty in @("requestBuilder")) {
|
||||||
|
$relativePath = $releaseContract.$pathProperty
|
||||||
|
$absolutePath = Join-Path $projectRoot $relativePath
|
||||||
|
if (-not (Test-Path -LiteralPath $absolutePath)) {
|
||||||
|
throw "EveTui release contract $pathProperty does not exist: $relativePath"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$testContract = $manifest.lifecycle.test.testContract
|
||||||
|
foreach ($field in @("ownerRepo", "runnerKind", "runnerScript", "consumerProject", "packageName", "testAssembly", "testPlatform", "resultsArtifact", "logArtifact", "manifestMutation")) {
|
||||||
|
if (-not $testContract.$field) {
|
||||||
|
throw "EveTui test contract missing $field"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($pathProperty in @("runnerScript", "consumerProject")) {
|
||||||
|
$candidate = $testContract.$pathProperty
|
||||||
|
$absolutePath = if ([System.IO.Path]::IsPathRooted($candidate)) {
|
||||||
|
$candidate
|
||||||
|
} else {
|
||||||
|
Join-Path $projectRoot $candidate
|
||||||
|
}
|
||||||
|
if (-not (Test-Path -LiteralPath $absolutePath)) {
|
||||||
|
throw "EveTui test contract $pathProperty does not exist: $candidate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$captureContract = $manifest.lifecycle.capture.captureContract
|
||||||
|
foreach ($field in @("ownerRepo", "runtimeId", "targetId", "requestSchema", "requestBuilder", "advertisementPath", "captureKind", "artifactKind", "artifactPattern", "conformanceAttachment", "requiredProvider", "requiredSurface", "authority", "publishProof")) {
|
||||||
|
if (-not $captureContract.$field) {
|
||||||
|
throw "EveTui capture contract missing $field"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($captureContract.runtimeId -ne "tui") {
|
||||||
|
throw "EveTui capture contract has unexpected runtime: $($captureContract.runtimeId)"
|
||||||
|
}
|
||||||
|
if ($captureContract.targetId -ne "tui") {
|
||||||
|
throw "EveTui capture contract has unexpected target: $($captureContract.targetId)"
|
||||||
|
}
|
||||||
|
if ($captureContract.requestSchema -ne "gamecult.eve.runtime_capture_request.v1") {
|
||||||
|
throw "EveTui capture contract has unexpected request schema: $($captureContract.requestSchema)"
|
||||||
|
}
|
||||||
|
if ($captureContract.captureKind -ne "terminal-cell-grid") {
|
||||||
|
throw "EveTui capture contract has unexpected capture kind: $($captureContract.captureKind)"
|
||||||
|
}
|
||||||
|
foreach ($pathProperty in @("requestBuilder", "advertisementPath")) {
|
||||||
|
$relativePath = $captureContract.$pathProperty
|
||||||
|
$absolutePath = Join-Path $projectRoot $relativePath
|
||||||
|
if (-not (Test-Path -LiteralPath $absolutePath)) {
|
||||||
|
throw "EveTui capture contract $pathProperty does not exist: $relativePath"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& (Join-Path $projectRoot "scripts\run-evetui-release-contract-smoke.ps1")
|
||||||
|
& (Join-Path $projectRoot "scripts\run-evetui-provider-shell-smoke.ps1")
|
||||||
|
& (Join-Path $projectRoot "scripts\run-evetui-capture-contract-smoke.ps1")
|
||||||
|
& (Join-Path $projectRoot "scripts\run-evetui-capture-smoke.ps1")
|
||||||
|
|
||||||
|
Write-Host "EveTui lifecycle smoke passed: $absoluteManifestPath"
|
||||||
111
scripts/run-evetui-provider-shell-smoke.ps1
Normal file
111
scripts/run-evetui-provider-shell-smoke.ps1
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
param(
|
||||||
|
[string] $CapabilityManifestPath = "eve-runtime-capability.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$projectRoot = Split-Path -Parent $PSScriptRoot
|
||||||
|
$absoluteManifestPath = if ([System.IO.Path]::IsPathRooted($CapabilityManifestPath)) {
|
||||||
|
$CapabilityManifestPath
|
||||||
|
} else {
|
||||||
|
Join-Path $projectRoot $CapabilityManifestPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $absoluteManifestPath)) {
|
||||||
|
throw "EveTui runtime capability manifest not found: $absoluteManifestPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
$manifest = Get-Content -LiteralPath $absoluteManifestPath -Raw | ConvertFrom-Json
|
||||||
|
|
||||||
|
if ($manifest.runtimeId -ne "tui") {
|
||||||
|
throw "Unexpected EveTui runtime id: $($manifest.runtimeId)"
|
||||||
|
}
|
||||||
|
if ($manifest.lifecycle.test.status -ne "provider-shell-contract-skeleton") {
|
||||||
|
throw "Unexpected EveTui provider shell status: $($manifest.lifecycle.test.status)"
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($feature in @("providerAdvertisements", "commandTransport", "terminalGridSummary", "terminalGridLowering", "pluginProjection")) {
|
||||||
|
if (-not (@($manifest.supportedFeatures) -contains $feature)) {
|
||||||
|
throw "EveTui provider shell missing supported feature: $feature"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($pluginId in @("sai.vn", "norn.graph", "tex.math")) {
|
||||||
|
$supported = @($manifest.supportedPlugins) | Where-Object { $_.pluginId -eq $pluginId } | Select-Object -First 1
|
||||||
|
if (-not $supported) {
|
||||||
|
throw "EveTui provider shell missing supported plugin projection: $pluginId"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$worldSurfaceLoweringClaims = @()
|
||||||
|
if ($null -ne $manifest.worldSurfaceLowering) {
|
||||||
|
$worldSurfaceLoweringClaims = @($manifest.worldSurfaceLowering)
|
||||||
|
}
|
||||||
|
if ($worldSurfaceLoweringClaims.Count -ne 1) {
|
||||||
|
throw "EveTui must claim exactly one provider world-surface lowering target"
|
||||||
|
}
|
||||||
|
$worldSurfaceLoweringClaim = $worldSurfaceLoweringClaims | Where-Object { $_.targetId -eq "tui" } | Select-Object -First 1
|
||||||
|
if (-not $worldSurfaceLoweringClaim) {
|
||||||
|
throw "EveTui missing tui world-surface lowering claim"
|
||||||
|
}
|
||||||
|
foreach ($surfaceKind in @("interactive-world", "interactive-world-editor")) {
|
||||||
|
if (-not (@($worldSurfaceLoweringClaim.surfaceKinds) -contains $surfaceKind)) {
|
||||||
|
throw "EveTui world-surface claim missing surface kind: $surfaceKind"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($projectionKind in @("provider-authored-world-surface", "provider-authored-world-editor-surface")) {
|
||||||
|
if (-not (@($worldSurfaceLoweringClaim.projectionKinds) -contains $projectionKind)) {
|
||||||
|
throw "EveTui world-surface claim missing projection kind: $projectionKind"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($worldSurfaceLoweringClaim.supportLevel -ne "terminal-grid-command-surface") {
|
||||||
|
throw "Unexpected EveTui support level: $($worldSurfaceLoweringClaim.supportLevel)"
|
||||||
|
}
|
||||||
|
if ($worldSurfaceLoweringClaim.ownership -ne "runtime-lowers-provider-world-surface-without-owning-world-state") {
|
||||||
|
throw "Unexpected EveTui ownership: $($worldSurfaceLoweringClaim.ownership)"
|
||||||
|
}
|
||||||
|
foreach ($evidencePath in @($worldSurfaceLoweringClaim.evidencePaths)) {
|
||||||
|
$absoluteEvidencePath = Join-Path $projectRoot $evidencePath
|
||||||
|
if (-not (Test-Path -LiteralPath $absoluteEvidencePath)) {
|
||||||
|
throw "EveTui world-surface claim missing evidence path: $evidencePath"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$expectedFiles = @(
|
||||||
|
"package.json",
|
||||||
|
"src\eve-tui-shell.mjs",
|
||||||
|
"test\eve-tui-shell.test.mjs"
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($relativePath in $expectedFiles) {
|
||||||
|
$absolutePath = Join-Path $projectRoot $relativePath
|
||||||
|
if (-not (Test-Path -LiteralPath $absolutePath)) {
|
||||||
|
throw "EveTui provider shell missing file: $relativePath"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$shellSource = Get-Content -LiteralPath (Join-Path $projectRoot "src\eve-tui-shell.mjs") -Raw
|
||||||
|
foreach ($symbol in @("EveTuiShell", "selectSurface", "lowerSurface", "normalizeSurfaceDocument", "buildComponentLines", "buildPluginProjection", "collectPluginProjections", "terminalElementKind", "sai.vn", "norn.graph", "tex.math", "sidecar-advertised-plugin-abi", "createCommandIntent", "renderSummary", "gamecult.eve.tui_grid.v1", "commandBoundary", "receiptSchema")) {
|
||||||
|
if (-not $shellSource.Contains($symbol)) {
|
||||||
|
throw "EveTui provider shell source missing symbol: $symbol"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$testSource = Get-Content -LiteralPath (Join-Path $projectRoot "test\eve-tui-shell.test.mjs") -Raw
|
||||||
|
foreach ($symbol in @("selects advertised provider surfaces", "command intents carry provider-advertised boundaries", "summary grid is explicitly lossy", "lowers provider surface trees into a terminal grid", "lowers Sai, Norn, and TeX plugin surfaces into compact terminal fallbacks", "pluginProjections", "sai-vn-terminal-stage-summary", "norn-graph-terminal-outline", "tex-math-terminal-block-source", "rejects surface documents that do not match the advertised TUI target", "aetheria.daemon.commands")) {
|
||||||
|
if (-not $testSource.Contains($symbol)) {
|
||||||
|
throw "EveTui provider shell test missing symbol: $symbol"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Push-Location $projectRoot
|
||||||
|
try {
|
||||||
|
npm test
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "EveTui provider shell tests failed with exit code $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "EveTui provider shell smoke passed: $absoluteManifestPath"
|
||||||
98
scripts/run-evetui-release-contract-smoke.ps1
Normal file
98
scripts/run-evetui-release-contract-smoke.ps1
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
param(
|
||||||
|
[string] $CapabilityManifestPath = "eve-runtime-capability.json",
|
||||||
|
[string] $OutputPath = "artifacts\evetui-release-contract\latest\release-request.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$projectRoot = Split-Path -Parent $PSScriptRoot
|
||||||
|
$absoluteCapabilityPath = if ([System.IO.Path]::IsPathRooted($CapabilityManifestPath)) {
|
||||||
|
$CapabilityManifestPath
|
||||||
|
} else {
|
||||||
|
Join-Path $projectRoot $CapabilityManifestPath
|
||||||
|
}
|
||||||
|
$absoluteOutputPath = if ([System.IO.Path]::IsPathRooted($OutputPath)) {
|
||||||
|
$OutputPath
|
||||||
|
} else {
|
||||||
|
Join-Path $projectRoot $OutputPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $absoluteCapabilityPath)) {
|
||||||
|
throw "EveTui release contract input not found: $absoluteCapabilityPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
$manifest = Get-Content -LiteralPath $absoluteCapabilityPath -Raw | ConvertFrom-Json
|
||||||
|
$releaseContract = $manifest.lifecycle.release.releaseContract
|
||||||
|
if ($null -eq $releaseContract) {
|
||||||
|
throw "EveTui release contract missing lifecycle.release.releaseContract"
|
||||||
|
}
|
||||||
|
foreach ($field in @("ownerRepo", "packageName", "packageRoot", "versionSource", "tagPattern", "artifactKind", "requestSchema", "requestBuilder", "artifactPattern", "publishProof")) {
|
||||||
|
if (-not $releaseContract.$field) {
|
||||||
|
throw "EveTui release contract missing $field"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($releaseContract.ownerRepo -ne "EveTui") {
|
||||||
|
throw "Unexpected EveTui release owner: $($releaseContract.ownerRepo)"
|
||||||
|
}
|
||||||
|
if ($releaseContract.packageName -ne "eve-tui") {
|
||||||
|
throw "Unexpected EveTui package name: $($releaseContract.packageName)"
|
||||||
|
}
|
||||||
|
if ($releaseContract.requestSchema -ne "gamecult.eve.runtime_release_request.v1") {
|
||||||
|
throw "Unexpected EveTui release request schema: $($releaseContract.requestSchema)"
|
||||||
|
}
|
||||||
|
if ($releaseContract.artifactKind -ne "terminal-runtime") {
|
||||||
|
throw "Unexpected EveTui release artifact kind: $($releaseContract.artifactKind)"
|
||||||
|
}
|
||||||
|
|
||||||
|
$builderPath = Join-Path $projectRoot $releaseContract.requestBuilder
|
||||||
|
$packagePath = Join-Path $projectRoot $releaseContract.versionSource
|
||||||
|
$packageRoot = Join-Path $projectRoot $releaseContract.packageRoot
|
||||||
|
foreach ($path in @($builderPath, $packagePath, $packageRoot)) {
|
||||||
|
if (-not (Test-Path -LiteralPath $path)) {
|
||||||
|
throw "EveTui release contract path not found: $path"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node --test (Join-Path $projectRoot "tools\evetui\evetui-release-contract.test.mjs")
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "EveTui release contract tests failed with exit code $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
|
||||||
|
node $builderPath --capability $absoluteCapabilityPath --package $packagePath --output $absoluteOutputPath
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "EveTui release request builder failed with exit code $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
if (-not (Test-Path -LiteralPath $absoluteOutputPath)) {
|
||||||
|
throw "EveTui release request was not written: $absoluteOutputPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
$request = Get-Content -LiteralPath $absoluteOutputPath -Raw | ConvertFrom-Json
|
||||||
|
$packageManifest = Get-Content -LiteralPath $packagePath -Raw | ConvertFrom-Json
|
||||||
|
foreach ($expectation in @(
|
||||||
|
@{ field = "schema"; value = "gamecult.eve.runtime_release_request.v1" },
|
||||||
|
@{ field = "ownerRepo"; value = "EveTui" },
|
||||||
|
@{ field = "repository"; value = "GameCult/EveTui" },
|
||||||
|
@{ field = "packageName"; value = "eve-tui" },
|
||||||
|
@{ field = "version"; value = $packageManifest.version },
|
||||||
|
@{ field = "packageRoot"; value = "." },
|
||||||
|
@{ field = "versionSource"; value = "package.json" },
|
||||||
|
@{ field = "artifactKind"; value = "terminal-runtime" }
|
||||||
|
)) {
|
||||||
|
if ($request.($expectation.field) -ne $expectation.value) {
|
||||||
|
throw "EveTui release request $($expectation.field) expected $($expectation.value) got $($request.($expectation.field))"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$expectedTagName = $releaseContract.tagPattern.Replace("{version}", $packageManifest.version)
|
||||||
|
if ($request.tagName -ne $expectedTagName) {
|
||||||
|
throw "Unexpected EveTui release tag: $($request.tagName)"
|
||||||
|
}
|
||||||
|
$expectedArtifactPath = $releaseContract.artifactPattern.Replace("{version}", $packageManifest.version)
|
||||||
|
if ($request.artifactPath -ne $expectedArtifactPath) {
|
||||||
|
throw "Unexpected EveTui release artifact path: $($request.artifactPath)"
|
||||||
|
}
|
||||||
|
if ($request.main -ne $packageManifest.main) {
|
||||||
|
throw "EveTui release request lost package main entry"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "EveTui release contract smoke passed: $absoluteOutputPath"
|
||||||
297
src/eve-tui-shell.mjs
Normal file
297
src/eve-tui-shell.mjs
Normal file
|
|
@ -0,0 +1,297 @@
|
||||||
|
export class EveTuiShell {
|
||||||
|
constructor({ clientId = "tui", width = 80 } = {}) {
|
||||||
|
this.clientId = clientId;
|
||||||
|
this.width = Math.max(20, Number(width) || 80);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectSurface(providerAdvertisement, requestedSurfaceId = "") {
|
||||||
|
const provider = normalizeProviderAdvertisement(providerAdvertisement);
|
||||||
|
const surface = requestedSurfaceId
|
||||||
|
? provider.surfaces.find(candidate => candidate.surfaceId === requestedSurfaceId)
|
||||||
|
: provider.surfaces[0];
|
||||||
|
if (!surface) {
|
||||||
|
throw new Error(`Provider ${provider.providerId} does not advertise surface ${requestedSurfaceId || "(first)"}`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
providerId: provider.providerId,
|
||||||
|
surfaceId: surface.surfaceId,
|
||||||
|
title: surface.title || provider.title || surface.surfaceId,
|
||||||
|
transport: surface.transport || "",
|
||||||
|
url: surface.url || surface.key || "",
|
||||||
|
worldInteraction: normalizeWorldInteraction(surface.worldInteraction),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
createCommandIntent(providerAdvertisement, requestedSurfaceId, command, payload = {}) {
|
||||||
|
if (!command) throw new Error("Command is required.");
|
||||||
|
const selected = this.selectSurface(providerAdvertisement, requestedSurfaceId);
|
||||||
|
const action = objectValue(payload.action);
|
||||||
|
const commandBoundary = firstString(
|
||||||
|
selected.worldInteraction.commandBoundary,
|
||||||
|
payload.commandBoundary,
|
||||||
|
action.commandBoundary,
|
||||||
|
action.target,
|
||||||
|
);
|
||||||
|
const receiptSchema = firstString(
|
||||||
|
selected.worldInteraction.receiptSchema,
|
||||||
|
payload.receiptSchema,
|
||||||
|
action.receiptSchema,
|
||||||
|
);
|
||||||
|
const intent = {
|
||||||
|
type: "surface-command",
|
||||||
|
schema: "gamecult.eve.command_invocation.v1",
|
||||||
|
providerId: selected.providerId,
|
||||||
|
surfaceId: selected.surfaceId,
|
||||||
|
command,
|
||||||
|
payload: {
|
||||||
|
...payload,
|
||||||
|
transport: payload.transport ?? null,
|
||||||
|
},
|
||||||
|
issuedAt: new Date().toISOString(),
|
||||||
|
clientId: this.clientId,
|
||||||
|
};
|
||||||
|
if (commandBoundary) intent.commandBoundary = commandBoundary;
|
||||||
|
if (receiptSchema) intent.receiptSchema = receiptSchema;
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
|
|
||||||
|
lowerSurface(surfaceDocument, providerAdvertisement, requestedSurfaceId = "") {
|
||||||
|
const selected = this.selectSurface(providerAdvertisement, requestedSurfaceId);
|
||||||
|
const document = normalizeSurfaceDocument(surfaceDocument);
|
||||||
|
if (document.surfaceId !== selected.surfaceId) {
|
||||||
|
throw new Error(`Surface document ${document.surfaceId} does not match advertised TUI surface ${selected.surfaceId}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
fitLine(`EveTui ${selected.providerId}`, this.width),
|
||||||
|
fitLine(`surface ${selected.surfaceId}`, this.width),
|
||||||
|
fitLine(`command ${selected.worldInteraction.commandBoundary || "unadvertised"}`, this.width),
|
||||||
|
fitLine(`receipt ${selected.worldInteraction.receiptSchema || "unadvertised"}`, this.width),
|
||||||
|
...buildComponentLines(document.root, this.width),
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
schema: "gamecult.eve.tui_grid.v1",
|
||||||
|
runtimeId: "tui",
|
||||||
|
providerId: selected.providerId,
|
||||||
|
surfaceId: selected.surfaceId,
|
||||||
|
projectionKind: selected.worldInteraction.projectionKind,
|
||||||
|
commandBoundary: selected.worldInteraction.commandBoundary,
|
||||||
|
receiptSchema: selected.worldInteraction.receiptSchema,
|
||||||
|
ownership: selected.worldInteraction.ownership,
|
||||||
|
width: this.width,
|
||||||
|
lines,
|
||||||
|
embeddedDocumentSlots: collectEmbeddedDocumentSlots(document.root),
|
||||||
|
pluginProjections: collectPluginProjections(document.root),
|
||||||
|
lossiness: "terminal-grid-command-surface; summarizes provider-authored world surface without owning provider state",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSummary(providerAdvertisement, requestedSurfaceId = "") {
|
||||||
|
const selected = this.selectSurface(providerAdvertisement, requestedSurfaceId);
|
||||||
|
const lines = [
|
||||||
|
fitLine(`EveTui ${selected.providerId}`, this.width),
|
||||||
|
fitLine(`surface ${selected.surfaceId}`, this.width),
|
||||||
|
fitLine(`transport ${selected.transport || "unknown"}`, this.width),
|
||||||
|
fitLine(`command ${selected.worldInteraction.commandBoundary || "unadvertised"}`, this.width),
|
||||||
|
fitLine(`receipt ${selected.worldInteraction.receiptSchema || "unadvertised"}`, this.width),
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
schema: "gamecult.eve.tui_grid.v1",
|
||||||
|
runtimeId: "tui",
|
||||||
|
providerId: selected.providerId,
|
||||||
|
surfaceId: selected.surfaceId,
|
||||||
|
width: this.width,
|
||||||
|
lines,
|
||||||
|
lossiness: "provider-shell-summary-only; use lowerSurface for TUI world-surface lowering",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeProviderAdvertisement(providerAdvertisement) {
|
||||||
|
if (!providerAdvertisement || typeof providerAdvertisement !== "object") {
|
||||||
|
throw new Error("Provider advertisement is required.");
|
||||||
|
}
|
||||||
|
const providerId = String(providerAdvertisement.providerId || "");
|
||||||
|
if (!providerId) throw new Error("Provider advertisement missing providerId.");
|
||||||
|
const surfaces = Array.isArray(providerAdvertisement.surfaces)
|
||||||
|
? providerAdvertisement.surfaces
|
||||||
|
.filter(surface => surface && typeof surface === "object" && surface.surfaceId)
|
||||||
|
.map(surface => ({ ...surface, surfaceId: String(surface.surfaceId) }))
|
||||||
|
: [];
|
||||||
|
if (!surfaces.length) throw new Error(`Provider ${providerId} advertises no surfaces.`);
|
||||||
|
return { ...providerAdvertisement, providerId, surfaces };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeSurfaceDocument(surfaceDocument) {
|
||||||
|
if (!surfaceDocument || typeof surfaceDocument !== "object") {
|
||||||
|
throw new Error("Surface document is required.");
|
||||||
|
}
|
||||||
|
if (surfaceDocument.schema !== "gamecult.eve.surface.v1") {
|
||||||
|
throw new Error(`Unexpected surface schema: ${surfaceDocument.schema || ""}`);
|
||||||
|
}
|
||||||
|
const surface = objectValue(surfaceDocument.surface);
|
||||||
|
const surfaceId = firstString(surface.id);
|
||||||
|
if (!surfaceId) throw new Error("Surface document missing surface.id.");
|
||||||
|
const root = objectValue(surface.root);
|
||||||
|
if (!root.id) throw new Error(`Surface document ${surfaceId} missing surface.root.`);
|
||||||
|
return { ...surfaceDocument, surfaceId, root };
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeWorldInteraction(value) {
|
||||||
|
const source = objectValue(value);
|
||||||
|
return {
|
||||||
|
projectionKind: firstString(source.projectionKind),
|
||||||
|
commandBoundary: firstString(source.commandBoundary),
|
||||||
|
receiptSchema: firstString(source.receiptSchema),
|
||||||
|
ownership: firstString(source.ownership),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildComponentLines(component, width, depth = 0) {
|
||||||
|
const source = objectValue(component);
|
||||||
|
const kind = firstString(source.kind) || "component";
|
||||||
|
const id = firstString(source.id) || "(anonymous)";
|
||||||
|
const props = objectValue(source.props);
|
||||||
|
const label = firstString(props.label, props.title, props.text, props.bind, props.command, props.fieldId);
|
||||||
|
const indent = " ".repeat(Math.min(depth, 6));
|
||||||
|
const pluginProjection = buildPluginProjection(kind, source);
|
||||||
|
const pluginSuffix = pluginProjection
|
||||||
|
? ` [${pluginProjection.pluginId}:${pluginProjection.semanticOwner}]`
|
||||||
|
: "";
|
||||||
|
const ownLine = fitLine(`${indent}${terminalElementKind(kind)} ${id}${label ? ` ${label}` : ""}${pluginSuffix}`, width);
|
||||||
|
const children = Array.isArray(source.children) ? source.children : [];
|
||||||
|
const embeddedDocuments = normalizeEmbeddedDocuments(source.embeddedDocuments);
|
||||||
|
return [
|
||||||
|
ownLine,
|
||||||
|
...embeddedDocuments.map(document => fitLine(`${indent} slot ${document.slotId || "(slot)"} ${document.documentId || "(document)"}`, width)),
|
||||||
|
...children.flatMap(child => buildComponentLines(child, width, depth + 1)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectEmbeddedDocumentSlots(component, slots = []) {
|
||||||
|
const source = objectValue(component);
|
||||||
|
const ownerId = firstString(source.id);
|
||||||
|
for (const document of normalizeEmbeddedDocuments(source.embeddedDocuments)) {
|
||||||
|
slots.push({ ownerId, ...document });
|
||||||
|
}
|
||||||
|
for (const child of Array.isArray(source.children) ? source.children : []) {
|
||||||
|
collectEmbeddedDocumentSlots(child, slots);
|
||||||
|
}
|
||||||
|
return slots;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectPluginProjections(component, projections = []) {
|
||||||
|
const source = objectValue(component);
|
||||||
|
const projection = buildPluginProjection(firstString(source.kind), source);
|
||||||
|
if (projection) {
|
||||||
|
projections.push({
|
||||||
|
ownerId: firstString(source.id),
|
||||||
|
...projection,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const child of Array.isArray(source.children) ? source.children : []) {
|
||||||
|
collectPluginProjections(child, projections);
|
||||||
|
}
|
||||||
|
return projections;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeEmbeddedDocuments(value) {
|
||||||
|
return Array.isArray(value)
|
||||||
|
? value
|
||||||
|
.filter(document => document && typeof document === "object")
|
||||||
|
.map(document => ({
|
||||||
|
slotId: firstString(document.slotId, document.id),
|
||||||
|
documentId: firstString(document.documentId, document.href, document.url),
|
||||||
|
schemaId: firstString(document.schemaId, document.schema),
|
||||||
|
presentationKind: firstString(document.presentationKind, document.kind),
|
||||||
|
}))
|
||||||
|
.filter(document => document.slotId || document.documentId)
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function terminalElementKind(componentKind) {
|
||||||
|
if (!componentKind) return "empty";
|
||||||
|
if (componentKind === "vn.stage") return "sai-vn";
|
||||||
|
if (componentKind === "embed.norn") return "norn";
|
||||||
|
if (componentKind === "embed.tex") return "tex";
|
||||||
|
if (componentKind.startsWith("control.")) return "command";
|
||||||
|
if (componentKind.startsWith("embed.")) return "plugin";
|
||||||
|
if (componentKind === "surface.slot") return "slot";
|
||||||
|
if (componentKind.startsWith("field.") || componentKind.startsWith("world.")) return "world";
|
||||||
|
if (componentKind.startsWith("text.") || componentKind === "text" || componentKind === "label") return "text";
|
||||||
|
if (componentKind === "metric") return "metric";
|
||||||
|
return componentKind;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPluginProjection(componentKind, component) {
|
||||||
|
const props = objectValue(component.props);
|
||||||
|
if (componentKind === "vn.stage") {
|
||||||
|
return {
|
||||||
|
pluginId: "sai.vn",
|
||||||
|
projectionKind: "sai-vn-terminal-stage-summary",
|
||||||
|
abiSchema: "gamecult.eve.plugin_abi.v1",
|
||||||
|
commandBoundary: "sidecar-advertised-plugin-abi",
|
||||||
|
capabilities: ["vn.stage", "story.choose", "story.continue", "story.jump"],
|
||||||
|
documentId: firstString(props.storyId, component.id),
|
||||||
|
semanticOwner: "Sai",
|
||||||
|
availability: "required",
|
||||||
|
fallbackKind: "terminal-summary",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (componentKind === "embed.norn") {
|
||||||
|
return {
|
||||||
|
pluginId: "norn.graph",
|
||||||
|
projectionKind: "norn-graph-terminal-outline",
|
||||||
|
abiSchema: "gamecult.eve.plugin_abi.v1",
|
||||||
|
commandBoundary: "sidecar-advertised-plugin-abi",
|
||||||
|
capabilities: ["embed.norn"],
|
||||||
|
documentId: firstString(props.sourceUri, props.documentId, component.id),
|
||||||
|
semanticOwner: "Norn",
|
||||||
|
availability: "optional-nested",
|
||||||
|
fallbackKind: "terminal-outline",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (componentKind === "embed.tex") {
|
||||||
|
return {
|
||||||
|
pluginId: "tex.math",
|
||||||
|
projectionKind: texProjectionKind(props),
|
||||||
|
abiSchema: "gamecult.eve.plugin_abi.v1",
|
||||||
|
commandBoundary: "sidecar-advertised-plugin-abi",
|
||||||
|
capabilities: ["embed.tex", "tex.inline", "tex.block"],
|
||||||
|
documentId: firstString(props.sourceUri, props.source, component.id),
|
||||||
|
semanticOwner: "EvePlugins",
|
||||||
|
availability: "optional-nested",
|
||||||
|
fallbackKind: "source-text",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function texProjectionKind(props) {
|
||||||
|
const display = firstString(props.display, "inline");
|
||||||
|
if (display === "block") return "tex-math-terminal-block-source";
|
||||||
|
if (display === "page") return "tex-math-terminal-page-source";
|
||||||
|
return "tex-math-terminal-inline-source";
|
||||||
|
}
|
||||||
|
|
||||||
|
function fitLine(value, width) {
|
||||||
|
const text = String(value || "");
|
||||||
|
if (text.length <= width) return text;
|
||||||
|
return `${text.slice(0, Math.max(0, width - 3))}...`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function objectValue(value) {
|
||||||
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstString(...values) {
|
||||||
|
for (const value of values) {
|
||||||
|
if (typeof value === "string" && value.trim()) return value;
|
||||||
|
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
71
test-support/schema-subset.mjs
Normal file
71
test-support/schema-subset.mjs
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
export function validateSchemaSubset(schema, value, pointer = "$", rootSchema = schema) {
|
||||||
|
const errors = [];
|
||||||
|
if (!schema || typeof schema !== "object") return errors;
|
||||||
|
|
||||||
|
if (schema.$ref) {
|
||||||
|
const resolved = resolveSchemaReference(rootSchema, schema.$ref);
|
||||||
|
return resolved
|
||||||
|
? validateSchemaSubset(resolved, value, pointer, rootSchema)
|
||||||
|
: [`${pointer}:unresolved-ref:${schema.$ref}`];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schema.const !== undefined && value !== schema.const) {
|
||||||
|
errors.push(`${pointer}:const:${schema.const}`);
|
||||||
|
}
|
||||||
|
if (schema.enum && !schema.enum.includes(value)) {
|
||||||
|
errors.push(`${pointer}:enum:${schema.enum.join("|")}`);
|
||||||
|
}
|
||||||
|
if (schema.type && !matchesSchemaType(value, schema.type)) {
|
||||||
|
errors.push(`${pointer}:type:${schema.type}`);
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
if (schema.minLength !== undefined && typeof value === "string" && value.length < schema.minLength) {
|
||||||
|
errors.push(`${pointer}:minLength:${schema.minLength}`);
|
||||||
|
}
|
||||||
|
if (schema.minItems !== undefined && Array.isArray(value) && value.length < schema.minItems) {
|
||||||
|
errors.push(`${pointer}:minItems:${schema.minItems}`);
|
||||||
|
}
|
||||||
|
if (schema.required && value && typeof value === "object" && !Array.isArray(value)) {
|
||||||
|
for (const key of schema.required) {
|
||||||
|
if (value[key] === undefined) errors.push(`${pointer}.${key}:required`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (schema.properties && value && typeof value === "object" && !Array.isArray(value)) {
|
||||||
|
for (const [key, childSchema] of Object.entries(schema.properties)) {
|
||||||
|
if (value[key] !== undefined) {
|
||||||
|
errors.push(...validateSchemaSubset(childSchema, value[key], `${pointer}.${key}`, rootSchema));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (schema.items && Array.isArray(value)) {
|
||||||
|
value.forEach((item, index) => {
|
||||||
|
errors.push(...validateSchemaSubset(schema.items, item, `${pointer}[${index}]`, rootSchema));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveSchemaReference(rootSchema, reference) {
|
||||||
|
if (!reference.startsWith("#/")) return null;
|
||||||
|
return reference.slice(2).split("/").reduce((current, segment) => {
|
||||||
|
if (!current || typeof current !== "object") return null;
|
||||||
|
return current[segment.replaceAll("~1", "/").replaceAll("~0", "~")];
|
||||||
|
}, rootSchema);
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesSchemaType(value, type) {
|
||||||
|
switch (type) {
|
||||||
|
case "array":
|
||||||
|
return Array.isArray(value);
|
||||||
|
case "object":
|
||||||
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||||
|
case "string":
|
||||||
|
return typeof value === "string";
|
||||||
|
case "number":
|
||||||
|
return typeof value === "number";
|
||||||
|
case "boolean":
|
||||||
|
return typeof value === "boolean";
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
256
test/eve-tui-shell.test.mjs
Normal file
256
test/eve-tui-shell.test.mjs
Normal file
|
|
@ -0,0 +1,256 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import test from "node:test";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { validateSchemaSubset } from "../test-support/schema-subset.mjs";
|
||||||
|
import { EveTuiShell } from "../src/eve-tui-shell.mjs";
|
||||||
|
|
||||||
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../Eve");
|
||||||
|
const tuiGridSchema = JSON.parse(readFileSync(
|
||||||
|
path.join(repoRoot, "schemas/gamecult.eve.tui_grid.v1.schema.json"),
|
||||||
|
"utf8",
|
||||||
|
));
|
||||||
|
|
||||||
|
test("selects advertised provider surfaces for compact projection", () => {
|
||||||
|
const shell = new EveTuiShell({ width: 48 });
|
||||||
|
const selected = shell.selectSurface(advertisement(), "aetheria.daemon.editor");
|
||||||
|
|
||||||
|
assert.equal(selected.providerId, "aetheria");
|
||||||
|
assert.equal(selected.surfaceId, "aetheria.daemon.editor");
|
||||||
|
assert.equal(selected.worldInteraction.commandBoundary, "aetheria.daemon.commands");
|
||||||
|
assert.equal(selected.worldInteraction.receiptSchema, "aetheria.eve_command_acceptance_status.v1");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("command intents carry provider-advertised boundaries", () => {
|
||||||
|
const shell = new EveTuiShell();
|
||||||
|
const intent = shell.createCommandIntent(
|
||||||
|
advertisement(),
|
||||||
|
"aetheria.daemon.game",
|
||||||
|
"aetheria.daemon.commands",
|
||||||
|
{
|
||||||
|
action: {
|
||||||
|
commandBoundary: "component.fallback.boundary",
|
||||||
|
receiptSchema: "component.fallback.receipt.v1",
|
||||||
|
target: "component.fallback.target",
|
||||||
|
},
|
||||||
|
commandId: "aetheria.daemon.focus",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(intent.schema, "gamecult.eve.command_invocation.v1");
|
||||||
|
assert.equal(intent.providerId, "aetheria");
|
||||||
|
assert.equal(intent.surfaceId, "aetheria.daemon.game");
|
||||||
|
assert.equal(intent.commandBoundary, "aetheria.daemon.commands");
|
||||||
|
assert.equal(intent.receiptSchema, "aetheria.eve_command_acceptance_status.v1");
|
||||||
|
assert.equal(intent.clientId, "tui");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("summary grid is explicitly lossy provider-shell evidence", () => {
|
||||||
|
const shell = new EveTuiShell({ width: 36 });
|
||||||
|
const grid = shell.renderSummary(advertisement(), "aetheria.daemon.game");
|
||||||
|
|
||||||
|
assert.deepEqual(validateSchemaSubset(tuiGridSchema, grid), []);
|
||||||
|
assert.equal(grid.schema, "gamecult.eve.tui_grid.v1");
|
||||||
|
assert.equal(grid.runtimeId, "tui");
|
||||||
|
assert.equal(grid.surfaceId, "aetheria.daemon.game");
|
||||||
|
assert.match(grid.lossiness, /use lowerSurface/);
|
||||||
|
assert.ok(grid.lines.every(line => line.length <= 36));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("lowers provider surface trees into a terminal grid", () => {
|
||||||
|
const shell = new EveTuiShell({ width: 44 });
|
||||||
|
const grid = shell.lowerSurface(surfaceDocument(), advertisement(), "aetheria.daemon.game");
|
||||||
|
|
||||||
|
assert.deepEqual(validateSchemaSubset(tuiGridSchema, grid), []);
|
||||||
|
assert.equal(grid.schema, "gamecult.eve.tui_grid.v1");
|
||||||
|
assert.equal(grid.runtimeId, "tui");
|
||||||
|
assert.equal(grid.providerId, "aetheria");
|
||||||
|
assert.equal(grid.surfaceId, "aetheria.daemon.game");
|
||||||
|
assert.equal(grid.projectionKind, "provider-authored-world-surface");
|
||||||
|
assert.equal(grid.commandBoundary, "aetheria.daemon.commands");
|
||||||
|
assert.equal(grid.receiptSchema, "aetheria.eve_command_acceptance_status.v1");
|
||||||
|
assert.match(grid.lossiness, /terminal-grid-command-surface/);
|
||||||
|
assert.ok(grid.lines.every(line => line.length <= 44));
|
||||||
|
assert.ok(grid.lines.some(line => line.includes("world aetheria.daemon.game.gravity")));
|
||||||
|
assert.ok(grid.lines.some(line => line.includes("command aetheria.daemon.game.focus")));
|
||||||
|
assert.ok(grid.lines.some(line => line.includes("norn aetheria.daemon.game.norn")));
|
||||||
|
assert.ok(grid.lines.some(line => line.includes("slot norn.map")));
|
||||||
|
assert.deepEqual(grid.pluginProjections, [
|
||||||
|
{
|
||||||
|
ownerId: "aetheria.daemon.game.norn",
|
||||||
|
pluginId: "norn.graph",
|
||||||
|
projectionKind: "norn-graph-terminal-outline",
|
||||||
|
abiSchema: "gamecult.eve.plugin_abi.v1",
|
||||||
|
commandBoundary: "sidecar-advertised-plugin-abi",
|
||||||
|
capabilities: ["embed.norn"],
|
||||||
|
documentId: "aetheria.daemon.game.norn",
|
||||||
|
semanticOwner: "Norn",
|
||||||
|
availability: "optional-nested",
|
||||||
|
fallbackKind: "terminal-outline",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
assert.deepEqual(grid.embeddedDocumentSlots, [
|
||||||
|
{
|
||||||
|
ownerId: "aetheria.daemon.game.norn",
|
||||||
|
slotId: "norn.map",
|
||||||
|
documentId: "cultmesh://aetheria/norn/map",
|
||||||
|
schemaId: "gamecult.eve.surface.v1",
|
||||||
|
presentationKind: "terminal-grid-slot",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects surface documents that do not match the advertised TUI target", () => {
|
||||||
|
const shell = new EveTuiShell();
|
||||||
|
assert.throws(
|
||||||
|
() => shell.lowerSurface(surfaceDocument("aetheria.daemon.game"), advertisement(), "aetheria.daemon.editor"),
|
||||||
|
/does not match advertised TUI surface/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("lowers Sai, Norn, and TeX plugin surfaces into compact terminal fallbacks", () => {
|
||||||
|
const shell = new EveTuiShell({ width: 72 });
|
||||||
|
const saiSurface = JSON.parse(readFileSync(path.join(repoRoot, "web/fixtures/sai-vn-surface.json"), "utf8"));
|
||||||
|
const grid = shell.lowerSurface(saiSurface, saiAdvertisement(), "sai.visual_novel.surface");
|
||||||
|
|
||||||
|
assert.deepEqual(validateSchemaSubset(tuiGridSchema, grid), []);
|
||||||
|
assert.equal(grid.providerId, "gamecult.home.vn");
|
||||||
|
assert.equal(grid.surfaceId, "sai.visual_novel.surface");
|
||||||
|
assert.ok(grid.lines.every(line => line.length <= 72));
|
||||||
|
assert.ok(grid.lines.some(line => line.includes("sai-vn sai.root")));
|
||||||
|
assert.ok(grid.lines.some(line => line.includes("norn sai.graph")));
|
||||||
|
assert.ok(grid.lines.some(line => line.includes("tex sai.tex.log-power")));
|
||||||
|
|
||||||
|
const byPlugin = new Map(grid.pluginProjections.map(projection => [projection.pluginId, projection]));
|
||||||
|
assert.equal(byPlugin.get("sai.vn").semanticOwner, "Sai");
|
||||||
|
assert.equal(byPlugin.get("sai.vn").availability, "required");
|
||||||
|
assert.equal(byPlugin.get("sai.vn").projectionKind, "sai-vn-terminal-stage-summary");
|
||||||
|
assert.deepEqual(byPlugin.get("sai.vn").capabilities, [
|
||||||
|
"vn.stage",
|
||||||
|
"story.choose",
|
||||||
|
"story.continue",
|
||||||
|
"story.jump",
|
||||||
|
]);
|
||||||
|
assert.equal(byPlugin.get("norn.graph").semanticOwner, "Norn");
|
||||||
|
assert.equal(byPlugin.get("norn.graph").availability, "optional-nested");
|
||||||
|
assert.equal(byPlugin.get("norn.graph").fallbackKind, "terminal-outline");
|
||||||
|
assert.equal(byPlugin.get("tex.math").semanticOwner, "EvePlugins");
|
||||||
|
assert.equal(byPlugin.get("tex.math").availability, "optional-nested");
|
||||||
|
assert.equal(byPlugin.get("tex.math").projectionKind, "tex-math-terminal-block-source");
|
||||||
|
assert.equal(byPlugin.get("tex.math").fallbackKind, "source-text");
|
||||||
|
assert.equal(byPlugin.get("tex.math").documentId, "\\\\mathrm{votes}(p)=1+\\\\lfloor\\\\log_b(1+p)\\\\rfloor");
|
||||||
|
});
|
||||||
|
|
||||||
|
function advertisement() {
|
||||||
|
return {
|
||||||
|
schema: "gamecult.eve.provider_advertisement.v1",
|
||||||
|
providerId: "aetheria",
|
||||||
|
title: "Aetheria",
|
||||||
|
surfaces: [
|
||||||
|
{
|
||||||
|
surfaceId: "aetheria.daemon.game",
|
||||||
|
transport: "local-json",
|
||||||
|
url: "conformance/eve/aetheria-world-surface.json",
|
||||||
|
worldInteraction: {
|
||||||
|
projectionKind: "provider-authored-world-surface",
|
||||||
|
commandBoundary: "aetheria.daemon.commands",
|
||||||
|
receiptSchema: "aetheria.eve_command_acceptance_status.v1",
|
||||||
|
ownership: "provider-owns-world-state-assets-command-acceptance-and-receipts",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
surfaceId: "aetheria.daemon.editor",
|
||||||
|
transport: "local-json",
|
||||||
|
url: "conformance/eve/aetheria-world-surface.json",
|
||||||
|
worldInteraction: {
|
||||||
|
projectionKind: "provider-authored-world-editor-surface",
|
||||||
|
commandBoundary: "aetheria.daemon.commands",
|
||||||
|
receiptSchema: "aetheria.eve_command_acceptance_status.v1",
|
||||||
|
ownership: "provider-owns-editor-state-assets-command-acceptance-and-receipts",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function saiAdvertisement() {
|
||||||
|
return {
|
||||||
|
schema: "gamecult.eve.provider_advertisement.v1",
|
||||||
|
providerId: "gamecult.home.vn",
|
||||||
|
title: "GameCult Compound VN",
|
||||||
|
surfaces: [
|
||||||
|
{
|
||||||
|
surfaceId: "sai.visual_novel.surface",
|
||||||
|
transport: "local-json",
|
||||||
|
url: "web/fixtures/sai-vn-surface.json",
|
||||||
|
worldInteraction: {
|
||||||
|
projectionKind: "provider-authored-world-surface",
|
||||||
|
commandBoundary: "sai.vn.plugin.commands",
|
||||||
|
receiptSchema: "gamecult.eve.command_receipt.v1",
|
||||||
|
ownership: "provider-owns-story-state-plugin-sidecars-own-nested-semantics",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function surfaceDocument(surfaceId = "aetheria.daemon.game") {
|
||||||
|
return {
|
||||||
|
type: "surface-state",
|
||||||
|
schema: "gamecult.eve.surface.v1",
|
||||||
|
providerId: "aetheria",
|
||||||
|
providerKind: "game.runtime",
|
||||||
|
title: "Aetheria world",
|
||||||
|
version: 1,
|
||||||
|
updatedAtUtc: "2026-07-09T00:00:00Z",
|
||||||
|
surface: {
|
||||||
|
id: surfaceId,
|
||||||
|
root: {
|
||||||
|
id: `${surfaceId}.root`,
|
||||||
|
kind: "surface",
|
||||||
|
props: {},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: `${surfaceId}.gravity`,
|
||||||
|
kind: "field.surface2d",
|
||||||
|
props: {
|
||||||
|
label: "Current Zone Gravity",
|
||||||
|
bind: "aetheria.daemon.soaView.gravityField",
|
||||||
|
fieldId: "aetheria.zone.gravity",
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `${surfaceId}.focus`,
|
||||||
|
kind: "control.button",
|
||||||
|
props: {
|
||||||
|
label: "Focus Relay",
|
||||||
|
command: "aetheria.daemon.commands",
|
||||||
|
commandId: "aetheria.daemon.focus",
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `${surfaceId}.norn`,
|
||||||
|
kind: "embed.norn",
|
||||||
|
props: {
|
||||||
|
label: "Norn tactical map",
|
||||||
|
},
|
||||||
|
embeddedDocuments: [
|
||||||
|
{
|
||||||
|
slotId: "norn.map",
|
||||||
|
documentId: "cultmesh://aetheria/norn/map",
|
||||||
|
schemaId: "gamecult.eve.surface.v1",
|
||||||
|
presentationKind: "terminal-grid-slot",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
styles: [],
|
||||||
|
},
|
||||||
|
commands: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
118
tools/evetui/evetui-capture-artifact.mjs
Normal file
118
tools/evetui/evetui-capture-artifact.mjs
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
import { mkdirSync, writeFileSync } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { EveTuiShell } from "../../src/eve-tui-shell.mjs";
|
||||||
|
import { buildTuiCaptureRequest, loadJsonFile, writeCaptureRequest } from "./evetui-capture-contract.mjs";
|
||||||
|
|
||||||
|
export function buildTuiCaptureArtifact({
|
||||||
|
advertisement,
|
||||||
|
capabilityManifest,
|
||||||
|
surfaceDocument,
|
||||||
|
advertisementPath = "",
|
||||||
|
capabilityManifestPath = "",
|
||||||
|
captureSurfaceId = "",
|
||||||
|
stamp = "latest",
|
||||||
|
width = 80,
|
||||||
|
} = {}) {
|
||||||
|
const request = buildTuiCaptureRequest({
|
||||||
|
advertisement,
|
||||||
|
capabilityManifest,
|
||||||
|
advertisementPath,
|
||||||
|
capabilityManifestPath,
|
||||||
|
captureSurfaceId,
|
||||||
|
stamp,
|
||||||
|
});
|
||||||
|
const shell = new EveTuiShell({ width });
|
||||||
|
const grid = shell.lowerSurface(surfaceDocument, advertisement, request.surfaceId);
|
||||||
|
validateGridArtifact(grid, request);
|
||||||
|
|
||||||
|
return {
|
||||||
|
request,
|
||||||
|
grid: {
|
||||||
|
...grid,
|
||||||
|
artifactKind: request.artifactKind,
|
||||||
|
captureKind: request.captureKind,
|
||||||
|
sourceAdvertisementPath: request.sourceAdvertisementPath,
|
||||||
|
sourceCapabilityManifestPath: request.sourceCapabilityManifestPath,
|
||||||
|
issuedAtUtc: request.issuedAtUtc,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeTuiCaptureArtifact({ request, grid }, { outputPath, requestOutputPath = "" } = {}) {
|
||||||
|
if (!outputPath) throw new Error("outputPath is required");
|
||||||
|
mkdirSync(path.dirname(outputPath), { recursive: true });
|
||||||
|
writeFileSync(outputPath, `${JSON.stringify(grid, null, 2)}\n`, "utf8");
|
||||||
|
if (requestOutputPath) {
|
||||||
|
writeCaptureRequest(request, requestOutputPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateGridArtifact(grid, request) {
|
||||||
|
if (grid.schema !== "gamecult.eve.tui_grid.v1") {
|
||||||
|
throw new Error(`Unexpected TUI grid schema: ${grid.schema || ""}`);
|
||||||
|
}
|
||||||
|
for (const [field, expected] of [
|
||||||
|
["runtimeId", request.runtimeId],
|
||||||
|
["providerId", request.providerId],
|
||||||
|
["surfaceId", request.surfaceId],
|
||||||
|
["projectionKind", request.projectionKind],
|
||||||
|
["commandBoundary", request.commandBoundary],
|
||||||
|
["receiptSchema", request.receiptSchema],
|
||||||
|
]) {
|
||||||
|
if ((grid[field] || "") !== (expected || "")) {
|
||||||
|
throw new Error(`TUI grid ${field} expected ${expected || ""} got ${grid[field] || ""}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!Array.isArray(grid.lines) || !grid.lines.length) {
|
||||||
|
throw new Error("TUI grid artifact must contain at least one line");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const args = {};
|
||||||
|
for (let index = 0; index < argv.length; index += 1) {
|
||||||
|
const value = argv[index];
|
||||||
|
if (!value.startsWith("--")) continue;
|
||||||
|
const key = value.slice(2);
|
||||||
|
const next = argv[index + 1];
|
||||||
|
if (!next || next.startsWith("--")) {
|
||||||
|
args[key] = true;
|
||||||
|
} else {
|
||||||
|
args[key] = next;
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runCli() {
|
||||||
|
const args = parseArgs(process.argv.slice(2));
|
||||||
|
for (const required of ["advertisement", "capability", "surface", "output"]) {
|
||||||
|
if (!args[required]) {
|
||||||
|
throw new Error(`missing required --${required}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const stamp = args.stamp || "latest";
|
||||||
|
const artifact = buildTuiCaptureArtifact({
|
||||||
|
advertisement: loadJsonFile(args.advertisement),
|
||||||
|
capabilityManifest: loadJsonFile(args.capability),
|
||||||
|
surfaceDocument: loadJsonFile(args.surface),
|
||||||
|
advertisementPath: args.advertisement,
|
||||||
|
capabilityManifestPath: args.capability,
|
||||||
|
captureSurfaceId: args["surface-id"] || "",
|
||||||
|
stamp,
|
||||||
|
width: Number(args.width || 80),
|
||||||
|
});
|
||||||
|
writeTuiCaptureArtifact(artifact, {
|
||||||
|
outputPath: args.output,
|
||||||
|
requestOutputPath: args["request-output"] || "",
|
||||||
|
});
|
||||||
|
process.stdout.write(`${args.output}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : "";
|
||||||
|
const modulePath = fileURLToPath(import.meta.url);
|
||||||
|
if (invokedPath === modulePath) {
|
||||||
|
runCli();
|
||||||
|
}
|
||||||
57
tools/evetui/evetui-capture-artifact.test.mjs
Normal file
57
tools/evetui/evetui-capture-artifact.test.mjs
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import test from "node:test";
|
||||||
|
import { buildTuiCaptureArtifact } from "./evetui-capture-artifact.mjs";
|
||||||
|
|
||||||
|
const advertisement = JSON.parse(readFileSync(new URL("../../../Aetheria/conformance/eve/aetheria.provider-advertisement.json", import.meta.url), "utf8"));
|
||||||
|
const capabilityManifest = JSON.parse(readFileSync(new URL("../../eve-runtime-capability.json", import.meta.url), "utf8"));
|
||||||
|
const surfaceDocument = JSON.parse(readFileSync(new URL("../../../Aetheria/conformance/eve/aetheria-world-surface.json", import.meta.url), "utf8"));
|
||||||
|
const worldSmokeAdvertisement = JSON.parse(readFileSync(new URL("../../../Eve/web/fixtures/eve-world-smoke.provider-advertisement.json", import.meta.url), "utf8"));
|
||||||
|
const worldSmokeSurfaceDocument = JSON.parse(readFileSync(new URL("../../../Eve/web/fixtures/eve-world-smoke-surface.json", import.meta.url), "utf8"));
|
||||||
|
|
||||||
|
test("builds TUI JSON grid capture artifact from provider advertisement and surface", () => {
|
||||||
|
const { request, grid } = buildTuiCaptureArtifact({
|
||||||
|
advertisement,
|
||||||
|
capabilityManifest,
|
||||||
|
surfaceDocument,
|
||||||
|
advertisementPath: "conformance/eve/aetheria.provider-advertisement.json",
|
||||||
|
capabilityManifestPath: "eve-runtime-capability.json",
|
||||||
|
stamp: "smoke",
|
||||||
|
width: 72,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(request.artifactKind, "json-grid");
|
||||||
|
assert.equal(request.artifactPath, "artifacts/evetui-capture/smoke/tui-grid.json");
|
||||||
|
assert.equal(grid.schema, "gamecult.eve.tui_grid.v1");
|
||||||
|
assert.equal(grid.runtimeId, "tui");
|
||||||
|
assert.equal(grid.providerId, "aetheria");
|
||||||
|
assert.equal(grid.surfaceId, "aetheria.daemon.game");
|
||||||
|
assert.equal(grid.commandBoundary, "aetheria.daemon.commands");
|
||||||
|
assert.equal(grid.receiptSchema, "aetheria.eve_command_acceptance_status.v1");
|
||||||
|
assert.equal(grid.artifactKind, "json-grid");
|
||||||
|
assert.equal(grid.captureKind, "terminal-cell-grid");
|
||||||
|
assert.equal(grid.width, 72);
|
||||||
|
assert.ok(grid.lines.some(line => line.includes("aetheria.daemon.game")));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("builds TUI JSON grid capture artifact from a generic world provider", () => {
|
||||||
|
const { grid } = buildTuiCaptureArtifact({
|
||||||
|
advertisement: worldSmokeAdvertisement,
|
||||||
|
capabilityManifest,
|
||||||
|
surfaceDocument: worldSmokeSurfaceDocument,
|
||||||
|
advertisementPath: "web/fixtures/eve-world-smoke.provider-advertisement.json",
|
||||||
|
capabilityManifestPath: "eve-runtime-capability.json",
|
||||||
|
stamp: "smoke",
|
||||||
|
width: 72,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(grid.schema, "gamecult.eve.tui_grid.v1");
|
||||||
|
assert.equal(grid.runtimeId, "tui");
|
||||||
|
assert.equal(grid.providerId, "eve.world-smoke");
|
||||||
|
assert.equal(grid.surfaceId, "eve.world-smoke.surface");
|
||||||
|
assert.equal(grid.commandBoundary, "eve.world-smoke.commands");
|
||||||
|
assert.equal(grid.receiptSchema, "eve.world_smoke.command_receipt.v1");
|
||||||
|
assert.equal(grid.artifactKind, "json-grid");
|
||||||
|
assert.equal(grid.captureKind, "terminal-cell-grid");
|
||||||
|
assert.ok(grid.lines.some(line => line.includes("eve.world-smoke.surface")));
|
||||||
|
});
|
||||||
160
tools/evetui/evetui-capture-contract.mjs
Normal file
160
tools/evetui/evetui-capture-contract.mjs
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
export function buildTuiCaptureRequest({
|
||||||
|
advertisement,
|
||||||
|
capabilityManifest,
|
||||||
|
advertisementPath = "",
|
||||||
|
capabilityManifestPath = "",
|
||||||
|
captureSurfaceId = "",
|
||||||
|
stamp = new Date().toISOString().replace(/[:.]/g, "-"),
|
||||||
|
} = {}) {
|
||||||
|
if (!advertisement || typeof advertisement !== "object") {
|
||||||
|
throw new Error("advertisement is required");
|
||||||
|
}
|
||||||
|
if (!capabilityManifest || typeof capabilityManifest !== "object") {
|
||||||
|
throw new Error("capabilityManifest is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const captureContract = capabilityManifest.lifecycle?.capture?.captureContract;
|
||||||
|
if (!captureContract) {
|
||||||
|
throw new Error("capability manifest is missing lifecycle.capture.captureContract");
|
||||||
|
}
|
||||||
|
|
||||||
|
const captureSurface = selectCaptureSurface(captureContract, advertisement.providerId, captureSurfaceId);
|
||||||
|
if (!captureSurface) {
|
||||||
|
if (captureSurfaceId) {
|
||||||
|
throw new Error(
|
||||||
|
`capture surface mismatch: expected ${captureSurfaceList(captureContract)} got ${advertisement.providerId || "missing"}:${captureSurfaceId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`capture provider mismatch: expected ${captureProviderList(captureContract)} got ${advertisement.providerId || "missing"}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetId = captureContract.targetId || captureContract.runtimeId;
|
||||||
|
const surface = (advertisement.surfaces || []).find(candidate => candidate.surfaceId === captureSurface.surfaceId);
|
||||||
|
if (!surface) {
|
||||||
|
throw new Error(`capture surface not advertised: ${captureSurface.surfaceId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const loweringTargets = surface.worldInteraction?.loweringTargets || [];
|
||||||
|
if (!loweringTargets.includes(targetId)) {
|
||||||
|
throw new Error(`surface ${surface.surfaceId} does not advertise lowering target ${targetId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const artifactPath = replaceStamp(captureContract.artifactPattern, stamp);
|
||||||
|
|
||||||
|
return {
|
||||||
|
schema: captureContract.requestSchema || "gamecult.eve.runtime_capture_request.v1",
|
||||||
|
runtimeId: capabilityManifest.runtimeId,
|
||||||
|
ownerRepo: captureContract.ownerRepo,
|
||||||
|
providerId: advertisement.providerId,
|
||||||
|
surfaceId: surface.surfaceId,
|
||||||
|
surfaceKind: surface.surfaceKind || "",
|
||||||
|
targetId,
|
||||||
|
projectionKind: surface.worldInteraction?.projectionKind || "",
|
||||||
|
commandBoundary: surface.worldInteraction?.commandBoundary || "",
|
||||||
|
receiptSchema: surface.worldInteraction?.receiptSchema || "",
|
||||||
|
captureKind: captureContract.captureKind,
|
||||||
|
artifactKind: captureContract.artifactKind,
|
||||||
|
artifactPath,
|
||||||
|
conformanceAttachment: captureContract.conformanceAttachment,
|
||||||
|
authority: captureContract.authority,
|
||||||
|
sourceAdvertisementPath: advertisementPath,
|
||||||
|
sourceCapabilityManifestPath: capabilityManifestPath,
|
||||||
|
issuedAtUtc: stamp,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadJsonFile(filePath) {
|
||||||
|
return JSON.parse(readFileSync(filePath, "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeCaptureRequest(request, outputPath) {
|
||||||
|
mkdirSync(path.dirname(outputPath), { recursive: true });
|
||||||
|
writeFileSync(outputPath, `${JSON.stringify(request, null, 2)}\n`, "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceStamp(pattern, stamp) {
|
||||||
|
if (!pattern || !pattern.includes("{stamp}")) {
|
||||||
|
throw new Error(`capture artifact pattern must include {stamp}: ${pattern || "missing"}`);
|
||||||
|
}
|
||||||
|
return pattern.replace("{stamp}", stamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectCaptureSurface(captureContract, providerId, surfaceId = "") {
|
||||||
|
const claims = captureSurfaceClaims(captureContract).filter(claim => claim.providerId === providerId);
|
||||||
|
if (surfaceId) return claims.find(claim => claim.surfaceId === surfaceId) || null;
|
||||||
|
return claims[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function captureProviderList(captureContract) {
|
||||||
|
return captureSurfaceClaims(captureContract).map(claim => claim.providerId).filter(Boolean).join(", ") || "missing";
|
||||||
|
}
|
||||||
|
|
||||||
|
function captureSurfaceList(captureContract) {
|
||||||
|
return captureSurfaceClaims(captureContract).map(claim => `${claim.providerId}:${claim.surfaceId}`).join(", ") || "missing";
|
||||||
|
}
|
||||||
|
|
||||||
|
function captureSurfaceClaims(captureContract) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
providerId: captureContract.requiredProvider || "",
|
||||||
|
surfaceId: captureContract.requiredSurface || "",
|
||||||
|
},
|
||||||
|
...(Array.isArray(captureContract.additionalProviderSurfaces) ? captureContract.additionalProviderSurfaces : []),
|
||||||
|
]
|
||||||
|
.filter(claim => claim && typeof claim === "object")
|
||||||
|
.map(claim => ({
|
||||||
|
providerId: String(claim.providerId || ""),
|
||||||
|
surfaceId: String(claim.surfaceId || ""),
|
||||||
|
}))
|
||||||
|
.filter(claim => claim.providerId && claim.surfaceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const args = {};
|
||||||
|
for (let index = 0; index < argv.length; index += 1) {
|
||||||
|
const value = argv[index];
|
||||||
|
if (!value.startsWith("--")) continue;
|
||||||
|
const key = value.slice(2);
|
||||||
|
const next = argv[index + 1];
|
||||||
|
if (!next || next.startsWith("--")) {
|
||||||
|
args[key] = true;
|
||||||
|
} else {
|
||||||
|
args[key] = next;
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runCli() {
|
||||||
|
const args = parseArgs(process.argv.slice(2));
|
||||||
|
for (const required of ["advertisement", "capability", "output"]) {
|
||||||
|
if (!args[required]) {
|
||||||
|
throw new Error(`missing required --${required}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stamp = args.stamp || new Date().toISOString().replace(/[:.]/g, "-");
|
||||||
|
const request = buildTuiCaptureRequest({
|
||||||
|
advertisement: loadJsonFile(args.advertisement),
|
||||||
|
capabilityManifest: loadJsonFile(args.capability),
|
||||||
|
advertisementPath: args.advertisement,
|
||||||
|
capabilityManifestPath: args.capability,
|
||||||
|
captureSurfaceId: args["surface-id"] || "",
|
||||||
|
stamp,
|
||||||
|
});
|
||||||
|
writeCaptureRequest(request, args.output);
|
||||||
|
process.stdout.write(`${args.output}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : "";
|
||||||
|
const modulePath = fileURLToPath(import.meta.url);
|
||||||
|
if (invokedPath === modulePath) {
|
||||||
|
runCli();
|
||||||
|
}
|
||||||
78
tools/evetui/evetui-capture-contract.test.mjs
Normal file
78
tools/evetui/evetui-capture-contract.test.mjs
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import test from "node:test";
|
||||||
|
import { buildTuiCaptureRequest } from "./evetui-capture-contract.mjs";
|
||||||
|
|
||||||
|
const advertisement = JSON.parse(readFileSync(new URL("../../../Aetheria/conformance/eve/aetheria.provider-advertisement.json", import.meta.url), "utf8"));
|
||||||
|
const worldSmokeAdvertisement = JSON.parse(readFileSync(new URL("../../../Eve/web/fixtures/eve-world-smoke.provider-advertisement.json", import.meta.url), "utf8"));
|
||||||
|
const capabilityManifest = JSON.parse(readFileSync(new URL("../../eve-runtime-capability.json", import.meta.url), "utf8"));
|
||||||
|
|
||||||
|
test("builds TUI capture request from provider advertisement", () => {
|
||||||
|
const request = buildTuiCaptureRequest({
|
||||||
|
advertisement,
|
||||||
|
capabilityManifest,
|
||||||
|
advertisementPath: "conformance/eve/aetheria.provider-advertisement.json",
|
||||||
|
capabilityManifestPath: "eve-runtime-capability.json",
|
||||||
|
stamp: "smoke",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(request.schema, "gamecult.eve.runtime_capture_request.v1");
|
||||||
|
assert.equal(request.runtimeId, "tui");
|
||||||
|
assert.equal(request.ownerRepo, "EveTui");
|
||||||
|
assert.equal(request.providerId, "aetheria");
|
||||||
|
assert.equal(request.surfaceId, "aetheria.daemon.game");
|
||||||
|
assert.equal(request.targetId, "tui");
|
||||||
|
assert.equal(request.projectionKind, "provider-authored-world-surface");
|
||||||
|
assert.equal(request.commandBoundary, "aetheria.daemon.commands");
|
||||||
|
assert.equal(request.receiptSchema, "aetheria.eve_command_acceptance_status.v1");
|
||||||
|
assert.equal(request.captureKind, "terminal-cell-grid");
|
||||||
|
assert.equal(request.artifactKind, "json-grid");
|
||||||
|
assert.equal(request.artifactPath, "artifacts/evetui-capture/smoke/tui-grid.json");
|
||||||
|
assert.match(request.authority, /provider state/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("builds TUI capture request for Aetheria editor surface", () => {
|
||||||
|
const request = buildTuiCaptureRequest({
|
||||||
|
advertisement,
|
||||||
|
capabilityManifest,
|
||||||
|
captureSurfaceId: "aetheria.daemon.editor",
|
||||||
|
advertisementPath: "conformance/eve/aetheria.provider-advertisement.json",
|
||||||
|
capabilityManifestPath: "eve-runtime-capability.json",
|
||||||
|
stamp: "smoke",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(request.runtimeId, "tui");
|
||||||
|
assert.equal(request.providerId, "aetheria");
|
||||||
|
assert.equal(request.surfaceId, "aetheria.daemon.editor");
|
||||||
|
assert.equal(request.surfaceKind, "interactive-world-editor");
|
||||||
|
assert.equal(request.projectionKind, "provider-authored-world-editor-surface");
|
||||||
|
assert.equal(request.commandBoundary, "aetheria.daemon.commands");
|
||||||
|
assert.equal(request.receiptSchema, "aetheria.eve_command_acceptance_status.v1");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("builds TUI capture request from a declared generic world provider", () => {
|
||||||
|
const request = buildTuiCaptureRequest({
|
||||||
|
advertisement: worldSmokeAdvertisement,
|
||||||
|
capabilityManifest,
|
||||||
|
advertisementPath: "web/fixtures/eve-world-smoke.provider-advertisement.json",
|
||||||
|
capabilityManifestPath: "eve-runtime-capability.json",
|
||||||
|
stamp: "smoke",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(request.runtimeId, "tui");
|
||||||
|
assert.equal(request.providerId, "eve.world-smoke");
|
||||||
|
assert.equal(request.surfaceId, "eve.world-smoke.surface");
|
||||||
|
assert.equal(request.commandBoundary, "eve.world-smoke.commands");
|
||||||
|
assert.equal(request.receiptSchema, "eve.world_smoke.command_receipt.v1");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects capture request when the surface does not advertise TUI", () => {
|
||||||
|
const withoutTarget = structuredClone(advertisement);
|
||||||
|
const surface = withoutTarget.surfaces.find(candidate => candidate.surfaceId === "aetheria.daemon.game");
|
||||||
|
surface.worldInteraction.loweringTargets = surface.worldInteraction.loweringTargets.filter(target => target !== "tui");
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() => buildTuiCaptureRequest({ advertisement: withoutTarget, capabilityManifest, stamp: "smoke" }),
|
||||||
|
/does not advertise lowering target tui/,
|
||||||
|
);
|
||||||
|
});
|
||||||
114
tools/evetui/evetui-release-contract.mjs
Normal file
114
tools/evetui/evetui-release-contract.mjs
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
export function buildTuiReleaseRequest({
|
||||||
|
capabilityManifest,
|
||||||
|
packageManifest,
|
||||||
|
capabilityManifestPath = "",
|
||||||
|
packageManifestPath = "",
|
||||||
|
repository = "GameCult/EveTui",
|
||||||
|
} = {}) {
|
||||||
|
if (!capabilityManifest || typeof capabilityManifest !== "object") {
|
||||||
|
throw new Error("capabilityManifest is required");
|
||||||
|
}
|
||||||
|
if (!packageManifest || typeof packageManifest !== "object") {
|
||||||
|
throw new Error("packageManifest is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const releaseContract = capabilityManifest.lifecycle?.release?.releaseContract;
|
||||||
|
if (!releaseContract) {
|
||||||
|
throw new Error("capability manifest is missing lifecycle.release.releaseContract");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packageManifest.name !== releaseContract.packageName) {
|
||||||
|
throw new Error(`package name mismatch: expected ${releaseContract.packageName} got ${packageManifest.name || "missing"}`);
|
||||||
|
}
|
||||||
|
if (!packageManifest.version || !/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(packageManifest.version)) {
|
||||||
|
throw new Error(`package version is not a SemVer-compatible release version: ${packageManifest.version || "missing"}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagName = replaceVersion(releaseContract.tagPattern, packageManifest.version);
|
||||||
|
const artifactPath = replaceVersion(
|
||||||
|
releaseContract.artifactPattern || `artifacts/evetui-release/{version}/${releaseContract.packageName}-{version}.tgz`,
|
||||||
|
packageManifest.version,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
schema: releaseContract.requestSchema || "gamecult.eve.runtime_release_request.v1",
|
||||||
|
ownerRepo: releaseContract.ownerRepo,
|
||||||
|
repository,
|
||||||
|
packageName: releaseContract.packageName,
|
||||||
|
version: packageManifest.version,
|
||||||
|
packageRoot: releaseContract.packageRoot,
|
||||||
|
versionSource: releaseContract.versionSource,
|
||||||
|
tagName,
|
||||||
|
artifactKind: releaseContract.artifactKind,
|
||||||
|
artifactPath,
|
||||||
|
publishProof: releaseContract.publishProof,
|
||||||
|
sourceCapabilityManifestPath: capabilityManifestPath,
|
||||||
|
sourcePackageManifestPath: packageManifestPath,
|
||||||
|
main: packageManifest.main || "",
|
||||||
|
scripts: packageManifest.scripts || {},
|
||||||
|
dependencies: packageManifest.dependencies || {},
|
||||||
|
devDependencies: packageManifest.devDependencies || {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadJsonFile(filePath) {
|
||||||
|
return JSON.parse(readFileSync(filePath, "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeReleaseRequest(request, outputPath) {
|
||||||
|
mkdirSync(path.dirname(outputPath), { recursive: true });
|
||||||
|
writeFileSync(outputPath, `${JSON.stringify(request, null, 2)}\n`, "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceVersion(pattern, version) {
|
||||||
|
if (!pattern || !pattern.includes("{version}")) {
|
||||||
|
throw new Error(`release pattern must include {version}: ${pattern || "missing"}`);
|
||||||
|
}
|
||||||
|
return pattern.replaceAll("{version}", version);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const args = {};
|
||||||
|
for (let index = 0; index < argv.length; index += 1) {
|
||||||
|
const value = argv[index];
|
||||||
|
if (!value.startsWith("--")) continue;
|
||||||
|
const key = value.slice(2);
|
||||||
|
const next = argv[index + 1];
|
||||||
|
if (!next || next.startsWith("--")) {
|
||||||
|
args[key] = true;
|
||||||
|
} else {
|
||||||
|
args[key] = next;
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runCli() {
|
||||||
|
const args = parseArgs(process.argv.slice(2));
|
||||||
|
for (const required of ["capability", "package", "output"]) {
|
||||||
|
if (!args[required]) {
|
||||||
|
throw new Error(`missing required --${required}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = buildTuiReleaseRequest({
|
||||||
|
capabilityManifest: loadJsonFile(args.capability),
|
||||||
|
packageManifest: loadJsonFile(args.package),
|
||||||
|
capabilityManifestPath: args.capability,
|
||||||
|
packageManifestPath: args.package,
|
||||||
|
repository: args.repository || "GameCult/EveTui",
|
||||||
|
});
|
||||||
|
writeReleaseRequest(request, args.output);
|
||||||
|
process.stdout.write(`${args.output}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : "";
|
||||||
|
const modulePath = fileURLToPath(import.meta.url);
|
||||||
|
if (invokedPath === modulePath) {
|
||||||
|
runCli();
|
||||||
|
}
|
||||||
37
tools/evetui/evetui-release-contract.test.mjs
Normal file
37
tools/evetui/evetui-release-contract.test.mjs
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import test from "node:test";
|
||||||
|
import { buildTuiReleaseRequest } from "./evetui-release-contract.mjs";
|
||||||
|
|
||||||
|
const capabilityManifest = JSON.parse(readFileSync(new URL("../../eve-runtime-capability.json", import.meta.url), "utf8"));
|
||||||
|
const packageManifest = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
|
||||||
|
|
||||||
|
test("builds EveTui release request from package manifest", () => {
|
||||||
|
const request = buildTuiReleaseRequest({
|
||||||
|
capabilityManifest,
|
||||||
|
packageManifest,
|
||||||
|
capabilityManifestPath: "eve-runtime-capability.json",
|
||||||
|
packageManifestPath: "package.json",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(request.schema, "gamecult.eve.runtime_release_request.v1");
|
||||||
|
assert.equal(request.ownerRepo, "EveTui");
|
||||||
|
assert.equal(request.repository, "GameCult/EveTui");
|
||||||
|
assert.equal(request.packageName, "eve-tui");
|
||||||
|
assert.equal(request.version, "0.1.0");
|
||||||
|
assert.equal(request.packageRoot, ".");
|
||||||
|
assert.equal(request.versionSource, "package.json");
|
||||||
|
assert.equal(request.tagName, "evetui-v0.1.0");
|
||||||
|
assert.equal(request.artifactKind, "terminal-runtime");
|
||||||
|
assert.equal(request.artifactPath, "artifacts/evetui-release/0.1.0/eve-tui-0.1.0.tgz");
|
||||||
|
assert.equal(request.main, "src/eve-tui-shell.mjs");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects package manifests that do not match the TUI release contract", () => {
|
||||||
|
const wrongPackage = { ...packageManifest, name: "eve-wrong" };
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() => buildTuiReleaseRequest({ capabilityManifest, packageManifest: wrongPackage }),
|
||||||
|
/package name mismatch/,
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in a new issue