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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use strict';
/* global Monogatari, monogatari */
const { $_ready, $_ } = Monogatari;
$_ready (() => {
try {
Promise.resolve(monogatari.init('#monogatari'))
.then(function () {
window.VeilNovelHost && window.VeilNovelHost.notify('ready');
})
.catch(function () {
window.VeilNovelHost && window.VeilNovelHost.notify('init-failed');
});
} catch (_) {
window.VeilNovelHost && window.VeilNovelHost.notify('init-failed');
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/* Monogatari v2.8.0 compatibility shim for iOS 15 WKWebView custom/file origins.
Engine files remain byte-for-byte identical to the official distribution. */
(function () {
'use strict';
if (typeof Object.hasOwn !== 'function') {
Object.hasOwn = function (object, key) {
return Object.prototype.hasOwnProperty.call(Object(object), key);
};
}
var rng = window.crypto;
if (rng && typeof rng.randomUUID !== 'function' && typeof rng.getRandomValues === 'function') {
// randomUUID() may be withheld in non-secure custom-scheme origins.
// Use only the platform's cryptographic random generator; never Math.random.
try {
Object.defineProperty(rng, 'randomUUID', {
configurable: true,
value: function () {
var bytes = new Uint8Array(16);
rng.getRandomValues(bytes);
bytes[6] = (bytes[6] & 15) | 64;
bytes[8] = (bytes[8] & 63) | 128;
var hex = [];
for (var i = 0; i < 16; i++) hex.push(bytes[i].toString(16).padStart(2, '0'));
return hex.slice(0, 4).join('') + '-' + hex.slice(4, 6).join('') + '-' +
hex.slice(6, 8).join('') + '-' + hex.slice(8, 10).join('') + '-' +
hex.slice(10).join('');
}
});
} catch (_) {
// If the host object is immutable, let the engine report the unsupported origin.
}
}
})();
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// R5: notification-only bridge; persistent-origin witness never reads game saves.
(function () {
'use strict';
var allowed = Object.freeze({
ready: true, error: true, 'init-failed': true, 'storage-unavailable': true,
'storage-probe-created': true, 'storage-probe-retained': true
});
var sent = Object.create(null);
var bridge = window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.veilNovel;
function notify(type) {
if (!Object.prototype.hasOwnProperty.call(allowed, type) || sent[type]) return;
sent[type] = true;
if (bridge && typeof bridge.postMessage === 'function') {
try { bridge.postMessage({ type: type }); } catch (_) { /* web preview has no native bridge */ }
}
}
Object.defineProperty(window, 'VeilNovelHost', {
value: Object.freeze({ protocolVersion: 3, notify: notify }),
configurable: false, writable: false
});
// Separate sentinel, not a Monogatari save key. This tests same-origin
// availability and allows the native host to compare across later launches.
try {
var storage = window.localStorage;
var healthKey = '__veilnovel_storage_probe_r5';
if (storage.getItem(healthKey) === 'veilnovel-v1') {
notify('storage-probe-retained');
} else {
storage.setItem(healthKey, 'veilnovel-v1');
if (storage.getItem(healthKey) === 'veilnovel-v1') {
notify('storage-probe-created');
} else {
notify('storage-unavailable');
}
}
} catch (_) { notify('storage-unavailable'); }
window.addEventListener('error', function () { notify('error'); });
window.addEventListener('unhandledrejection', function () { notify('error'); });
})();
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Foundation

func check(_ header: String?, _ length: Int, _ start: Int?, _ end: Int?) {
let actual = VeilNovelRangeParser.resolve(header, fileLength: length)
if let start = start, let end = end {
precondition(actual?.first == start, "bad start for \(header ?? "nil")")
precondition(actual?.last == end, "bad end for \(header ?? "nil")")
} else { precondition(actual == nil, "expected rejection: \(header ?? "nil")") }
}
check(nil, 100, 0, 99)
check("bytes=0-0", 100, 0, 0)
check("bytes=13-", 100, 13, 99)
check("bytes=-10", 100, 90, 99)
check("bytes=-999", 100, 0, 99)
check("bytes=90-300", 100, 90, 99)
check("bytes=99-99", 100, 99, 99)
check("bytes=100-", 100, nil, nil)
check("bytes=12-11", 100, nil, nil)
check("bytes=1-2,3-4", 100, nil, nil)
check("bytes=1-xyz", 100, nil, nil)
check("bytes=18446744073709551616-", 100, nil, nil)
check("bytes=-0", 100, nil, nil)
check("BYTES=1-5", 100, nil, nil)
check(nil, 0, nil, nil)
print("PASS: 15 Swift Foundation range-parser cases")
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Foundation

/// RFC 7233 single-byte-range subset for a read-only local bundle server.
/// Deliberately rejects multiple/ambiguous ranges rather than guessing.
struct VeilNovelResolvedByteRange: Equatable {
let first: Int
let last: Int // inclusive
let fullLength: Int

var count: Int { last - first + 1 }
var contentRange: String { "bytes \(first)-\(last)/\(fullLength)" }
}

enum VeilNovelRangeParser {
static func resolve(_ header: String?, fileLength: Int) -> VeilNovelResolvedByteRange? {
guard fileLength > 0 else { return nil }
guard let header = header else {
return VeilNovelResolvedByteRange(first: 0, last: fileLength - 1, fullLength: fileLength)
}
let value = header.trimmingCharacters(in: .whitespaces)
guard value.hasPrefix("bytes="), !value.contains(",") else { return nil }
let spec = String(value.dropFirst(6))
let parts = spec.split(separator: "-", omittingEmptySubsequences: false)
guard parts.count == 2 else { return nil }
func digits(_ s: Substring) -> Int? {
guard !s.isEmpty, s.utf8.allSatisfy({ $0 >= 48 && $0 <= 57 }) else { return nil }
return Int(s)
}
if parts[0].isEmpty {
guard let suffix = digits(parts[1]), suffix > 0 else { return nil }
return VeilNovelResolvedByteRange(first: max(0, fileLength - suffix), last: fileLength - 1, fullLength: fileLength)
}
guard let first = digits(parts[0]), first < fileLength else { return nil }
let end: Int
if parts[1].isEmpty { end = fileLength - 1 }
else {
guard let explicit = digits(parts[1]), explicit >= first else { return nil }
end = min(explicit, fileLength - 1)
}
return VeilNovelResolvedByteRange(first: first, last: end, fullLength: fileLength)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# R3 Inbox manifest
- agent_id: game-agent
- task_id: 20261025-monogatari-ios15-r3
- base_main_sha: 501ef814a0a9bea41419fe2d9f48315371b30320
- submission_path: AGENT_INBOX/submissions/game-agent/20261025-monogatari-ios15-r3/
- mode: INBOX_ONLY
- actual_status: OWNER_PACKAGE_COMPLETED; GITHUB_PARTIAL_SOURCE_STAGING_ONLY
- complete_owner_package: VeilLink_Monogatari_OfficialFullEngine_R3_20261025.zip (5,636,992 bytes)
- complete_owner_package_sha256: 840270de26bcf53ea1f8fe68e4d8ec717ae610d289c1de6dfa7573c59bd2cd09
- upstream ZIP sha256: 16d221485888b0193312e9989ecafa57cb1913978bf2f09ba554552ba3885806
- no product/main/release/ipa action: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
diff --git a/VeilLink/Narrative/VeilMonogatariByteRange.swift b/VeilLink/Narrative/VeilMonogatariByteRange.swift
new file mode 100644
--- /dev/null
+++ b/VeilLink/Narrative/VeilMonogatariByteRange.swift
@@ -0,0 +1,42 @@
+import Foundation
+
+/// RFC 7233 single-byte-range subset for a read-only local bundle server.
+/// Deliberately rejects multiple/ambiguous ranges rather than guessing.
+struct VeilNovelResolvedByteRange: Equatable {
+ let first: Int
+ let last: Int // inclusive
+ let fullLength: Int
+
+ var count: Int { last - first + 1 }
+ var contentRange: String { "bytes \(first)-\(last)/\(fullLength)" }
+}
+
+enum VeilNovelRangeParser {
+ static func resolve(_ header: String?, fileLength: Int) -> VeilNovelResolvedByteRange? {
+ guard fileLength > 0 else { return nil }
+ guard let header = header else {
+ return VeilNovelResolvedByteRange(first: 0, last: fileLength - 1, fullLength: fileLength)
+ }
+ let value = header.trimmingCharacters(in: .whitespaces)
+ guard value.hasPrefix("bytes="), !value.contains(",") else { return nil }
+ let spec = String(value.dropFirst(6))
+ let parts = spec.split(separator: "-", omittingEmptySubsequences: false)
+ guard parts.count == 2 else { return nil }
+ func digits(_ s: Substring) -> Int? {
+ guard !s.isEmpty, s.utf8.allSatisfy({ $0 >= 48 && $0 <= 57 }) else { return nil }
+ return Int(s)
+ }
+ if parts[0].isEmpty {
+ guard let suffix = digits(parts[1]), suffix > 0 else { return nil }
+ return VeilNovelResolvedByteRange(first: max(0, fileLength - suffix), last: fileLength - 1, fullLength: fileLength)
+ }
+ guard let first = digits(parts[0]), first < fileLength else { return nil }
+ let end: Int
+ if parts[1].isEmpty { end = fileLength - 1 }
+ else {
+ guard let explicit = digits(parts[1]), explicit >= first else { return nil }
+ end = min(explicit, fileLength - 1)
+ }
+ return VeilNovelResolvedByteRange(first: first, last: end, fullLength: fileLength)
+ }
+}
diff --git a/Tools/VNPort/tests/swift/main.swift b/Tools/VNPort/tests/swift/main.swift
new file mode 100644
--- /dev/null
+++ b/Tools/VNPort/tests/swift/main.swift
@@ -0,0 +1,25 @@
+import Foundation
+
+func check(_ header: String?, _ length: Int, _ start: Int?, _ end: Int?) {
+ let actual = VeilNovelRangeParser.resolve(header, fileLength: length)
+ if let start = start, let end = end {
+ precondition(actual?.first == start, "bad start for \(header ?? "nil")")
+ precondition(actual?.last == end, "bad end for \(header ?? "nil")")
+ } else { precondition(actual == nil, "expected rejection: \(header ?? "nil")") }
+}
+check(nil, 100, 0, 99)
+check("bytes=0-0", 100, 0, 0)
+check("bytes=13-", 100, 13, 99)
+check("bytes=-10", 100, 90, 99)
+check("bytes=-999", 100, 0, 99)
+check("bytes=90-300", 100, 90, 99)
+check("bytes=99-99", 100, 99, 99)
+check("bytes=100-", 100, nil, nil)
+check("bytes=12-11", 100, nil, nil)
+check("bytes=1-2,3-4", 100, nil, nil)
+check("bytes=1-xyz", 100, nil, nil)
+check("bytes=18446744073709551616-", 100, nil, nil)
+check("bytes=-0", 100, nil, nil)
+check("BYTES=1-5", 100, nil, nil)
+check(nil, 0, nil, nil)
+print("PASS: 15 Swift Foundation range-parser cases")
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Full official Monogatari v2.8.0: iOS15 reliability R3
Owner requested additional optimization after R2 full upstream binary import.
This PR stages an actual **Foundation byte-range parser** plus 15-scenario executable test in FILES/PATCH.diff.
The complete user-facing R3 ZIP contains remaining modified **native SwiftUI/WebKit source, full unmodified upstream engine, source vendor, 94-file package**, original official release ZIP, iOS host adapter and all validation programs. Those additional sources and binary assets are **not** stored in this GitHub Inbox proposal, and must be ingested separately by Governor.

R3 candidate improvements: 256-KiB bounded asset streaming, single Range 206 / invalid 416, path+size guards, WebContent process termination manual recovery, init-failure handling, current-session LocalStorage availability warning, de-duplicate/allowlist JS->native messages, disable debug JS execution (while retaining debug.js bytes) and disable eager preload.

Current tests: exact official archive 2,798,775 bytes SHA match, 56 source files retained, all 12 upstream engine blobs unchanged, 15 Swift Foundation range tests PASS, 19 story nodes / 4 endings PASS, bridge+error tests PASS, fake integration checkout dry-run/apply PASS, source-only patch apply check PASS. Not Apple SDK or physical device tests.

Priority: Resolve iPhone7 P0 touch/signing and #64 #65 #66 before product promotion. No direct change to chat/SQLite/Keychain/BLE/wire/A9/A10.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Monogatari R4 Playability Compatibility Addendum — 2026-10-25

This is an update to **the same Game Agent R3 Inbox lane**. It is not a functional merged game, not an IPA, and must not be self-merged.

## Complete owner-delivered candidate
- Artifact: `VeilLink_Monogatari_Playability_R4_20261025.zip` (in the current ChatGPT conversation).
- ZIP bytes: **5,674,020**; SHA-256: `8a0b419d5184edbb41c99ab0cfc4935b14125ced586e2c200c5bd747d18dfa72`.
- 111 archive entries; original official ZIP SHA-256 `16d221485888b0193312e9989ecafa57cb1913978bf2f09ba554552ba3885806`; entire upstream `engine/` subtree remains unmodified.
- Full R4 SwiftUI host, scripts, CSS, optional file loading mode, test suites and tools are in that **user-facing ZIP**, not all in this PR.

## Actual new code staged in GitHub
- `FILES/Tools/VNPort/overlays/js/veillink-ios15-compat.js`: executable additive Safari/WebKit origin compatibility polyfill for missing `crypto.randomUUID` and `Object.hasOwn`, never using Math.random.
- `R4_PATCH.diff`: proposed add-only source patch for this shim. Governor may accept after review.

## Other improvements only in the full owner ZIP
1. Native WKWebView manual `loadFileURL(_:allowingReadAccessTo:)` fallback when custom-scheme navigation/init fails; file and custom scheme may have separate save namespaces.
2. 20-second startup deadline, explicit user reload, stale coordinator event invalidation.
3. JS native notification bridge is loaded before JS main init; compatibility shim before official engine.
4. Small-screen touch targets, keyboard focus and reduce-motion style overlay.
5. XcodeGen nested `type: folder` reference check and conservative optional repair for **generated .pbxproj**.
6. Reproducible genuine ZIP reimport (60 game files), negative integrity tests and corrected movable integration tests.

## Local validation achieved
- True official release ZIP SHA256 PASS; 56 upstream files present; 12 engine binaries byte-for-byte PASS.
- All 60 regenerated game files match R4 candidate; 19 nodes/3 choices/4 endings story graph.
- 15 Swift byte-range tests, 5 PBX path fixtures, JS bridge/launch-order/UUID shims, mock integration dry run/apply/reapply rejection PASS.
- Swift parser only; NOT Apple SDK SwiftUI typecheck.
- Browser smoke attempt blocked by execution environment's administrator policy. NO iPhone 7 device runtime, offline save/restore, touch or audio test has run.

## Hard acceptance gates
Prioritize #64/#65/#66 iOS15 startup/gesture/data P0. On a separate Governor integration branch ingest **verified complete owner ZIP**, inspect XcodeGen compiled .app folder tree, run macOS iOS15/XCTest and physical iPhone7 dark/light/gesture/save/relaunch/branch/media validation. DO NOT merge main, change signing/version, or dispatch IPA Action until gates pass and owner authorizes release.

**Status: SOURCE CANDIDATE VERIFIED; FULL GAME PLAYABILITY UNCONFIRMED.**
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
diff --git a/Tools/VNPort/overlays/js/veillink-ios15-compat.js b/Tools/VNPort/overlays/js/veillink-ios15-compat.js
new file mode 100644
--- /dev/null
+++ b/Tools/VNPort/overlays/js/veillink-ios15-compat.js
@@ -0,0 +1,33 @@
+/* Monogatari v2.8.0 compatibility shim for iOS 15 WKWebView custom/file origins.
+ Engine files remain byte-for-byte identical to the official distribution. */
+(function () {
+ 'use strict';
+ if (typeof Object.hasOwn !== 'function') {
+ Object.hasOwn = function (object, key) {
+ return Object.prototype.hasOwnProperty.call(Object(object), key);
+ };
+ }
+ var rng = window.crypto;
+ if (rng && typeof rng.randomUUID !== 'function' && typeof rng.getRandomValues === 'function') {
+ // randomUUID() may be withheld in non-secure custom-scheme origins.
+ // Use only the platform's cryptographic random generator; never Math.random.
+ try {
+ Object.defineProperty(rng, 'randomUUID', {
+ configurable: true,
+ value: function () {
+ var bytes = new Uint8Array(16);
+ rng.getRandomValues(bytes);
+ bytes[6] = (bytes[6] & 15) | 64;
+ bytes[8] = (bytes[8] & 63) | 128;
+ var hex = [];
+ for (var i = 0; i < 16; i++) hex.push(bytes[i].toString(16).padStart(2, '0'));
+ return hex.slice(0, 4).join('') + '-' + hex.slice(4, 6).join('') + '-' +
+ hex.slice(6, 8).join('') + '-' + hex.slice(8, 10).join('') + '-' +
+ hex.slice(10).join('');
+ }
+ });
+ } catch (_) {
+ // If the host object is immutable, let the engine report the unsupported origin.
+ }
+ }
+})();
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# R5 PLAYABILITY HARDENING — 2026-10-25

**Verified owner-facing complete package:** `VeilLink_Monogatari_Playability_R5_20261025.zip`
- exact size **5,697,469 bytes**
- SHA-256 `068ba55bfa36672d61960240a684dc4d90ceb7a10868409baf73bc4e6ac6c6f1`
- 115 packaged files; 114 per-file SHA256SUMS entries validated.
- upstream `monogatari-v2.8.0.zip` verified SHA256 `16d221485888b0193312e9989ecafa57cb1913978bf2f09ba554552ba3885806` and **12/12 pristine upstream engine files**.

## Actual R5 fixes in owner's ZIP
1. Load native error bridge **before** the official engine, so boot exceptions aren't invisible.
2. Monogatari init Promise: catch both direct synchronous throw and rejected async result.
3. Independent `localStorage` witness keyed only to VN application metadata; per-origin native UserDefaults expectation warns if the marker disappears. Never reads, rewrites, imports or deletes actual Monogatari saved games. A marker surviving **does not prove actual saved-game durability**.
4. Verify both essential JS glue files exist before game navigation; handle post-commit WebKit navigation failures as well as provisional errors.
5. Keep complete upstream engine unmodified and preserve explicit dual loading modes, no automatic cross-origin migration.

## Executed local tests
- 5 Python R4 regression suites PASS
- 5 Node suites including R5 bridge + 3 init outcomes PASS
- 15 Swift Foundation Range cases PASS; Swift iOS sources **syntax parsed only**
- real vendor ZIP rebuilt into 60 bit-identical game files PASS
- game graph 19 nodes, 3 choices, 4 endings and 36 states PASS
- git apply --check / whitespace=error for complete source-only candidate patch PASS
- Browser was **BLOCKED_BY_ADMINISTRATOR** for localhost, data:, and file: URLs: **no game UI/interaction run**.
- **No macOS iOS15 build, XCTest with Apple SDK, physical iPhone7 gameplay or real save/load after force-quit.**

## Governance and completeness
This Draft PR only stages **text-only partial source + auditable receipt**. Complete modified SwiftUI/WebKit host, full vendor and JS sources and binaries are in exact owner's R5 ZIP, NOT in this PR. Governor must verify/import the ZIP on an isolated integration branch, reconcile P0 #64/#65/#66 touch/signing/data continuity, XcodeGen bundle resource structure, build full Apple iOS15/XCTest, and conduct physical iPhone7 validation. **Do not merge main or dispatch IPA/release.**
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# R3 risk review
- The GITHUB submission is intentionally partial (ByteRange parser and its tests plus contract); full binary/Swift host sources remain in owner's exact ZIP, **not merged**.
- On iOS15 custom WKURLSchemeHandler may have different Range/cancellation/AVFoundation behavior than simulated; HTTPURLResponse on non-http schemes untested with Apple SDK.
- Custom-origin localStorage probe only tests current-session write/read, not storage durability across relaunch; no false persisted-save guarantee.
- iOS SDK Swift/WebKit typecheck and iPhone7 playback, memory, touch and process-recovery tests are still pending.
- GameLobby/project.yml changes overlap existing other feature lanes; Governor only decides integration order.
- Do not modify main, workflows, signing, identity, BLE, database or releases; no IPA dispatch.
Loading
Loading