P18-01-CODE: 原本関係モデル・旧値保全・Runtime保存と読取経路

This commit is contained in:
Agent Control Runner
2026-10-15 20:33:19 +09:00
parent 0084760ee0
commit b7a7cc01d4
13 changed files with 2132 additions and 29 deletions
+5 -9
View File
@@ -1056,14 +1056,10 @@ func _restore_snapshot_internal(snapshot: Dictionary, recovery_mode: bool) -> bo
return false return false
var restored_relationships := RelationshipService.new() var restored_relationships := RelationshipService.new()
restored_relationships.configure_original_identity_validator( restored_relationships.configure_original_identity_validator(
func(payload: Dictionary) -> bool: return _original_relationship_identities_valid(payload, restored_staff) func(payload: Dictionary) -> bool: return _original_relationship_identities_valid(payload, restored_staff, restored_catalog)
) )
if not restored_relationships.restore_snapshot(runtime_data[&"relationships"]): if not restored_relationships.restore_snapshot(runtime_data[&"relationships"]):
return false return false
for relationship: RelationshipState in restored_relationships.get_relationships():
for actor_id: StringName in [relationship.actor_a_id, relationship.actor_b_id]:
if actor_id != &"player" and not restored_staff.has_staff(actor_id) and not restored_guests.has_guest(actor_id):
return false
var restored_events := EventService.new() var restored_events := EventService.new()
for raw_definition: Variant in runtime_data[&"event_definitions"]: for raw_definition: Variant in runtime_data[&"event_definitions"]:
var definition := EventDefinition.from_variant(raw_definition) var definition := EventDefinition.from_variant(raw_definition)
@@ -5387,7 +5383,7 @@ func _configure_relationship_event_boundaries() -> void:
if relationship_service == null or event_service == null: if relationship_service == null or event_service == null:
return return
relationship_service.configure_original_identity_validator( relationship_service.configure_original_identity_validator(
func(payload: Dictionary) -> bool: return _original_relationship_identities_valid(payload, staff_service) func(payload: Dictionary) -> bool: return _original_relationship_identities_valid(payload, staff_service, mvp_content_catalog)
) )
relationship_service.configure_candidate_callback( relationship_service.configure_candidate_callback(
Callable(self, "_on_relationship_threshold_candidate") Callable(self, "_on_relationship_threshold_candidate")
@@ -6770,15 +6766,15 @@ func _execute_progress_staff_task(command: SimulationCommand) -> CommandResult:
result.command_id = command.command_id result.command_id = command.command_id
return result return result
func _original_relationship_identities_valid(payload: Dictionary, staff_registry: StaffService) -> bool: func _original_relationship_identities_valid(payload: Dictionary, staff_registry: StaffService, catalog: MvpContentCatalog) -> bool:
if payload.get("protagonist_id") != "player" or staff_registry == null: if payload.get("protagonist_id") != "player" or staff_registry == null:
return false return false
var subject := StringName(payload.get("subject_id", "")) var subject := StringName(payload.get("subject_id", ""))
var rival := StringName(payload.get("rival_id", "")) var rival := StringName(payload.get("rival_id", ""))
if subject == &"player" or rival == &"player" or subject == rival or not staff_registry.has_staff(subject): if subject == &"player" or rival == &"player" or subject == rival or not staff_registry.has_staff(subject):
return false return false
var heroine_definition := mvp_content_catalog.get_entry(&"unique_characters", subject) var heroine_definition := catalog.get_entry(&"unique_characters", subject)
var rival_definition := mvp_content_catalog.get_entry(&"unique_characters", rival) var rival_definition := catalog.get_entry(&"unique_characters", rival)
var subject_staff := staff_registry.get_staff(subject) var subject_staff := staff_registry.get_staff(subject)
return subject_staff.employment_kind == StaffRuntime.EmploymentKind.UNIQUE \ return subject_staff.employment_kind == StaffRuntime.EmploymentKind.UNIQUE \
and heroine_definition.get(&"role_id") == String(subject_staff.role_id) \ and heroine_definition.get(&"role_id") == String(subject_staff.role_id) \
@@ -7,7 +7,7 @@
"template": "Original relationship model: <checks> checks, <failures> failures", "template": "Original relationship model: <checks> checks, <failures> failures",
"required": true, "required": true,
"fixed_check_count_used": false, "fixed_check_count_used": false,
"source_sha256_at_schema_audit": "ad509f5fb67ab91e1d1d99a08af51e4695e411b9a2541ea63ee22b1555cb0924", "source_sha256_at_schema_audit": "c74b243dbbb54dba9bbc165e29508cca910f0beb63e32d25ebbb8ea606dccce9",
"authority": { "authority": {
"path": "docs/spec/acceptance/P18-01-code-r01.json", "path": "docs/spec/acceptance/P18-01-code-r01.json",
"scope": "new backend suite marker; native execution pending" "scope": "new backend suite marker; native execution pending"
@@ -18,7 +18,7 @@
"template": "Original relationship runtime: <checks> checks, <failures> failures", "template": "Original relationship runtime: <checks> checks, <failures> failures",
"required": true, "required": true,
"fixed_check_count_used": false, "fixed_check_count_used": false,
"source_sha256_at_schema_audit": "374f3d31a65d2f2b62dfef29721c8c838575b856f4fa5a0bca31c6ba4c7e09dd", "source_sha256_at_schema_audit": "845ae200575563400c3497e517137effb7d4d928dd7e6a4990ed8aabdbda5ae6",
"authority": { "authority": {
"path": "docs/spec/acceptance/P18-01-code-r01.json", "path": "docs/spec/acceptance/P18-01-code-r01.json",
"scope": "new backend suite marker; native execution pending" "scope": "new backend suite marker; native execution pending"
@@ -1,6 +1,53 @@
# P18-01-CODE implementation handoff # P18-01-CODE implementation handoff
Status: submitted for Runner validation; no acceptance or unit completion claimed. Status: repair 5 submitted for Root native and Runner verification; no acceptance or unit completion claimed. P02-01 inputs are now readable and verified. The repair-3/4 notes below are retained history; repair 5 supersedes their input availability gap.
## Repair 3 historical failure (2026-10-15)
Historical record only: stopping independent repairs over decoder permissiveness was wrong. The commander producer-evidence interpretation supersedes this old policy concern; the following describes the failed repair-3 attempt, not the current policy.
Inspected baseline d2a1f80a613e7c77b02b941026bc49c2d85db863 using read-only git show.
RelationshipState.from_snapshot delegates to configure; _value preserves canonical
key precedence. Numeric fields use int(), with clampi/maxi where applicable.
Thus finite fractional numbers are truncated as well as integral JSON floats;
numeric strings such as metrics.affection = "42" are also converted. The baseline
RelationshipService._parse_snapshot similarly converts version and candidate
sequence with int(). These are broader than a finite-integral-float-only policy.
No acceptance policy for these conflicting formats has been selected here.
Baseline normalization also replaces non-Dictionary metrics, action_counts,
daily_metric_deltas, flags and crossed_thresholds with defaults, coerces flags
with bool(), and ignores non-Array threshold collections. The immutable spec
requires wrong types to reject, while the repair direction requires baseline
legitimate acceptance without a new restriction or loosening. In particular,
accepting or rejecting numeric strings cannot satisfy both without an explicit
policy decision. Non-numeric/non-finite input, wrong containers, future versions,
duplicates, invalid references, corrupt original records and projection mismatches
remain required rejections under the spec; this report does not waive any.
Baseline Runtime restore delegates legacy relationships to RelationshipService
without the added actor-existence loop. Baseline GuestService.despawn_guest retains
the departed guest in persistent_records and removes it from active guests.
The held actor loop is therefore still an unresolved repair item. Candidate
catalog selection, lifecycle tests, mutation-through-alias tests, atomic rejection
coverage and source-pin updates are also still outstanding. The numeric Reader
helpers in SimulationSnapshot and SaveService accept bounded finite integral JSON
floats for their own envelope fields; they do not establish a policy overriding
RelationshipState's coercions. P02-01 representative-save applicability has not
been established; no fixture acceptance claim is made.
Only this handoff and prepare-only evidence were changed. Production code, test
sources and marker/policy pins were left untouched pending policy resolution.
Prepare-only exited 0: 1,311 source files, 67,310,742 bytes; 3,384,279,040 free
bytes; 381,883,542 conservative required bytes; capacity available. Seven suites:
original_relationship_model, original_relationship_runtime,
relationship_event_service, runtime_relationship_events,
runtime_snapshot_serialization, runtime_save_lifecycle, staff_management.
308 future parse paths and nine future native phases; engines/stages = 0,
actual_checks = null, formal_acceptance = false. The first invocation omitted
required --output and exited 2; the corrected invocation used the allowed
p18-01-code/repair-3-prepare output path. Native checks were not run.
P18-01 remains 30/88, in progress, with no partial credit.
Scope: dedicated original relationship state, lossless legacy snapshot preservation, Scope: dedicated original relationship state, lossless legacy snapshot preservation,
Runtime registration and read projection, and the contract-listed tests and collector. Runtime registration and read projection, and the contract-listed tests and collector.
@@ -34,8 +81,8 @@ The acceptance specification without its final newline matches the contract SHA2
catalog membership does not invent recruitment. No new original update policy, catalog membership does not invent recruitment. No new original update policy,
contact, route, attack, threshold or duel behavior is defined. contact, route, attack, threshold or duel behavior is defined.
- Snapshot restore validates candidates before replacing live services, then - Snapshot restore validates candidates before replacing live services, then
rebinds the identity validator to live staff. Existing legacy actor references rebinds the identity validator to live staff. Legacy relationships retain the original acceptance of departed guests; original
must resolve against player, restored staff or restored guests. records separately validate staff/person/catalog references.
- Every staff-management row exposes `original_relationship`. Missing records - Every staff-management row exposes `original_relationship`. Missing records
have `configured: false` and null values, distinct from explicit zero. Configured have `configured: false` and null values, distinct from explicit zero. Configured
projections are copies, so reads and caller edits cannot change service state. projections are copies, so reads and caller edits cannot change service state.
@@ -77,3 +124,195 @@ in_progress: ordinary Japanese UI rendering/equality, approved production values
all-seven recruitment, final full required roster and applicable Windows final all-seven recruitment, final full required roster and applicable Windows final
acceptance remain outstanding. No git writes, other agents or Runner controls acceptance remain outstanding. No git writes, other agents or Runner controls
were used by worker. were used by worker.
## Repair 4 producer interpretation and independent repairs
The unchanged wrong-types/valid-legacy contract follows legitimate producers,
not corrupt-input coercions. RelationshipState.create_snapshot
(scripts/relationship/relationship_state.gd:132-152) emits String ids/route,
int numeric fields, named route_lock, Dictionary numeric/bool collections and
Array thresholds. Its validity check (:99-101) requires int metrics.
Read-only git log identifies 423fd6d as the only history entry for that file;
git show of that producer (:132-152) also emits the enum NAME String, not int.
Actual writer scripts/core/save_service.gd:874-878 serializes the snapshot with
var_to_bytes and ZSTD; Reader :1021-1028 uses bytes_to_var. Thus world numeric
ints remain ints. JSON decoding (:970-978) applies to the manifest, not relationship
payloads. Integral JSON floats are not an established relationship save format
on this path. Accept int legacy values, named route_lock and producer collections.
Reject numeric strings, null, bool-as-number, fractional/nonfinite numbers, float
relationship numbers, int route_lock, wrong containers and invalid enum names.
Unknown extensions remain opaque; canonical values precede aliases, and aliases,
unknown facts and collection order remain raw retained facts. Neither old axis
configures original Affection/Lust/Netorare.
relationship_service.gd _legacy_types_valid now also checks collection key types
and restricts route_lock to producer enum names. The pre-existing int/container
checks implement the producer policy. No conversion or rewrite of raw input occurs.
Runtime candidate validation now takes restored_catalog; command/service binding
uses the live catalog. Removed only the added active legacy actor loop.
GuestService.despawn_guest (scripts/guest/guest_service.gd:954-978) retains the
persistent record and removes the active guest, so that loop rejected valid saves.
Original identity/person validation remains in place.
Existing StaffService.remove_staff (:897-898) and retire_staff (:942-943) already
refuse UNIQUE departures before changes; tests assert refusal and saved original
record survival. No new departure rule or record deletion was added.
The two new suites now cover real Service and Runtime affection/dependence
mutations from v1 affinity/obligation, canonical precedence, raw extensions and
order, null original values, v2 restore/rebind, departed persistent guest acceptance,
and atomic malformed candidates. Rejection checks retain service identity/state,
whole Runtime bytes and caller input. The five existing suites remain unchanged.
Fixtures are validation inputs, not production values or newly hired characters.
Collector derives every sorted .gd path from the exact shared.source_refs map.
Verification binds parse-paths.json, the receipt count, the unique zero-failure
FULL_DEPENDENCY_PARSE marker, actual_parse_checks and the whole retained stage
to that same source map. No count-specific skips or other provenance changes.
This frozen map has 308 paths; expanded maps must verify their own full count.
### Precise remaining context gap
The P02-01 input manifest referenced by
docs/plans/original-completion-2026-10-02/execution/multi-agent-dev-handoff/delivery_contract_proposal.json:333
is absent at artifacts/original-completion-2026-10-02/p02-01-fixed-baseline-r01/input-manifest.json
in this worktree. Reading the exact production-root counterpart returns
Operation not permitted. Production-root listing/search is also denied.
No representative save bytes or decoded fixture are available in this checkout;
.review-material is absent. Accordingly their exact types/applicability have NOT
been verified, no older conflicting producer shape is asserted, and no replacement
representative fixture has been invented. Runner/owner must supply readable pinned
manifest and applicable save members to finish that mandatory evidence and test.
This gap did not stop the independent repairs. No native checks were executed.
UI, values, routes, contacts and duel remain untouched. P18-01 remains 30/88,
in progress. Native seven-suite frozen evidence, p18-model validation, separate
Opus review, latest-main integration and fresh merged-source evidence remain
Root/Runner work; no completion claim is made.
### Worker static/prepare evidence (repair 4)
Python AST parsed successfully. git diff --check passed. Both new suite SHA-256
pins match their final source in marker contracts and suite-policy; all other
rows are unchanged. The five existing suite files are byte-identical to HEAD.
No engine, native collector, default profile or full regression was run.
Prepare-only roster (seven): original_relationship_model,
original_relationship_runtime, relationship_event_service,
runtime_relationship_events, runtime_snapshot_serialization,
runtime_save_lifecycle, staff_management. Full paths are the unchanged ROSTER in
tools/run_p18_relationship_tests.py:17-25.
Initial repair-4 preview: 1,311 files, 67,318,770 source bytes,
3,388,039,168 free bytes, conservative single-set admission 381,891,570 bytes,
308 parse paths, nine future phases, engines/stages zero, actual_checks null,
formal_acceptance false. A final preview after final edits is retained separately
as repair-4-prepare.json. No native pass is implied by this preview.
Using the supplied old output estimate conservatively as 474 MiB per full set
(including 351 MiB engine), two fresh sets require 948 MiB = 994,050,048 bytes.
They fit the observed ~3.39 GB free space, with ~2.39 GB left before other new
consumers. This is capacity arithmetic, not a reservation or a measured new run;
actual output growth remains Root's admission responsibility.
## Repair 5: readable fixed inputs and representative binding
All work continues from preserved HEAD 0084760ee05a97adfcbd783a71161491f3629918.
Before edits, the nine dirty files were hashed; exact paths and SHA-256 are in
repair-5-start-shas.json. The command direction supplies no nine-file publication
hash table, and no publication table is present in the supplied input context.
Therefore full publication equality cannot independently be asserted. Both
existing test hashes matched their repair-4 marker/policy pins. Current bytes
were preserved and independent work continued; no reset/reimplementation occurred.
Only the supplied p02-01-fixed-inputs-for-p18-r05 context was used for external
input. No production absolute path in the manifest was followed. Manifest SHA:
d07b6c42ecca11bf3557ee5034ce9cc70b5a750f7b08b6b40f9ef09cdbe787c2.
ROOT_COPY_PROVENANCE.json's manifest hash, all 28 copy-file size/hash entries,
and all 20 original member size/hash entries verified successfully. No missing
file or mismatch occurred. Exact per-member evidence is retained in
repair-5-input-evidence.json; inspect_repair5_inputs.py reproduces the read-only
verification and equivalent little-endian typed Variant decoding. It checks
TTWB/version/header size, compressed-world checksum, ZSTD expansion length and
complete Variant stream consumption. Unsupported types fail explicitly.
### Observed package evidence
All five contain a Dictionary v1 relationship payload with an Array
relationships collection, empty; threshold_candidates is empty, and version and
next_candidate_sequence are typed ints. Metric/shield/day fields are absent
because there are no relationship records: their Variant types are **unobserved**,
not float, int, null or guessed defaults.
| Package | world.bin bytes | Expanded Variant bytes | world.bin SHA-256 |
| --- | ---: | ---: | --- |
| initial | 7523 | 61132 | df91b4624c9afdeea2d366ff38d9f545fcff3f5f99223e3c04ae66d04b2f9fab |
| operating | 48631 | 1020364 | 93732bb0bc042f4da0f90879a03dc3f3e374b3b45000840b0cbae142c42601c6 |
| moving | 27413 | 390624 | a577b190cb3b18cfc5cca1ef15d26521ea950bf5f3d3da68d19b1c8f43672158 |
| date-boundary | 26686 | 579376 | d4443f0bd42c2e9d6a5dd07d1949deade5d7526036f1320b56336b5637c9fcc3 |
| defense | 10110 | 78904 | 34ab140d64743b89425c292b785827fdb82c2bf92510688a4038461a2fe7a5d7 |
### Type decision and applicability
Actual producer scripts/core/save_service.gd:874 uses var_to_bytes(snapshot);
:878 compresses ZSTD. Actual Reader :1021-1028 expands then bytes_to_var.
Manifest JSON (:1754) carries identity metadata, not relationship values.
RelationshipState.create_snapshot (scripts/relationship/relationship_state.gd:132-152)
emits typed int metric/shield/day values. No actual relationship JSON-float path
was found. Keep the repair-4 int-only legacy checks closed to floats
(scripts/relationship/relationship_service.gd:544-587); no production type change
was required. Strings, null, bool-as-number, floats including NaN/Inf and wrong
containers remain rejected. No synthetic float producer was invented.
The empty real collections prove compatibility only for those actual snapshots;
they do not prove every non-empty metric shape, alias or historical producer.
Existing focused alias/mutation fixtures and explicit original fixtures provide
non-empty coverage and remain validation inputs, never production approval.
Binding: all five exact world.bin files are embedded in the already source-hashed
test_original_relationship_runtime.gd REPRESENTATIVE_SAVES constant. TTWB payloads
are already ZSTD compressed, so the base64 contains compressed bytes, not expanded
Variant streams. Embedded manifests retain the actual checksums. Each test verifies
world.bin size and full SHA before writing only into its owned user://saves/p18_*
fixture directory. It invokes the actual SaveService._load_snapshot_absolute_v1
Reader with validation, then actual Runtime.restore_snapshot. It asserts exact
legacy facts, null/unconfigured original projections (including the no-staff case),
input preservation, read nonmutation, actual SaveService writer/Reader v2 relationship
round trip and Runtime equality. Physical package format is selected by SaveService;
relationship v2 does not invent a new physical save format.
Corruption candidates derive from each decoded representative, changing the actual
relationships collection to String, Dictionary or null, and assert rejection
preserves service identity, service/whole-Runtime bytes and input bytes.
Empty representatives cannot provide corrupt per-record metric examples; existing
focused non-empty numeric-string/container tests cover those shapes separately.
Fixture package directories are removed after each applicable test. No loose
fixture binaries or collector fixture-map exceptions were added. All source-map,
stage, per-phase/final inventory guards and source-proof semantics are unchanged.
The five existing suites remain byte-identical to HEAD. Production scripts,
original values, UI, routes, contacts and duel are untouched in repair 5.
Both new-test pins in marker contracts and suite-policy are refreshed to exact
final source digests. Native suites and GDScript engine parse remain unexecuted.
P18-01 remains **30/88, in progress**. Fresh native seven, strict p18-model,
separate Opus review, latest-main integration and fresh merged native evidence
are the next Root/Runner gates, not backend acceptance or unit completion.
### Repair-5 static/prepare result
Python AST, independent extraction of all five embedded compressed streams and
their size/SHA checks, exact marker/policy pins, unchanged non-P18 policy/marker
records, five existing suite byte equality, and git diff --check passed.
Preserved Runtime/service/model/collector and repair-4-prepare hashes still match
the repair-5 starting bytes. No engine, native collector, full regression, Runner,
git write, branch/main operation or additional agent was used.
Final prepare-only receipt is repair-5-prepare.json. Roster (7):
original_relationship_model, original_relationship_runtime,
relationship_event_service, runtime_relationship_events,
runtime_snapshot_serialization, runtime_save_lifecycle, staff_management.
The exact shared source_refs map yields 1,311 files, 67,498,284 bytes and 308
sorted .gd parse paths; nine future native phases. Empty actual checks,
engines/stages = 0 and formal_acceptance = false.
Free space at preview: 3,390,361,600 bytes (about 3.39 GB); single-set admission
382,071,084 bytes. At the conservative prior estimate of 474 MiB per set,
two fresh native sets require 994,050,048 bytes and fit, leaving about 2.39 GB.
Capacity remains a point-in-time preview, not reserved disk or native evidence.
@@ -0,0 +1,94 @@
"""Read-only equivalent of SaveService's ZSTD/Variant reader; no engine invocation."""
import base64
import hashlib
import json
from pathlib import Path
import struct
import subprocess
CONTEXT = Path('/Users/korizatou114/Projects/TheTree-managed-worktrees/p18-01-model/.runtime/P18-01-CODE/build/p02-01-fixed-inputs-for-p18-r05')
HERE = Path(__file__).parent
class VariantReader:
def __init__(self, data):
self.data, self.offset = data, 0
def unpack(self, fmt):
value = struct.unpack_from('<' + fmt, self.data, self.offset)[0]
self.offset += struct.calcsize('<' + fmt)
return value
def read(self):
header = self.unpack('I')
kind = header & 0xffff
wide = bool(header & 0x10000)
if kind == 0:
return None
if kind == 1:
return bool(self.unpack('I'))
if kind == 2:
return self.unpack('q' if wide else 'i')
if kind == 3:
return self.unpack('d' if wide else 'f')
if kind in (5, 6, 7, 8, 9, 10, 12, 13):
count = {5: 2, 6: 2, 7: 4, 8: 4, 9: 3, 10: 3, 12: 4, 13: 4}[kind]
fmt = 'i' if kind in (6, 8, 10, 13) else ('d' if wide else 'f')
return tuple(self.unpack(fmt) for _ in range(count))
if kind in (4, 21):
size = self.unpack('I')
value = self.data[self.offset:self.offset + size].decode('utf-8')
self.offset += (size + 3) & ~3
return value
if kind == 27:
count = self.unpack('I') & 0x7fffffff
result = {}
for _ in range(count):
key = self.read()
result[key] = self.read()
return result
if kind == 28:
if (header >> 16) & 3:
assert (header >> 16) & 3 == 1, 'only builtin typed arrays supported'
self.unpack('I') # element Variant type, values remain individually tagged
return [self.read() for _ in range(self.unpack('I') & 0x7fffffff)]
raise ValueError(f'unsupported Variant {kind} at {self.offset - 4}')
def inspect():
manifest_bytes = (CONTEXT / 'input-manifest.json').read_bytes()
provenance = json.loads((CONTEXT / 'ROOT_COPY_PROVENANCE.json').read_bytes())
digest = hashlib.sha256(manifest_bytes).hexdigest()
assert digest == provenance['original_manifest_sha256'] == 'd07b6c42ecca11bf3557ee5034ce9cc70b5a750f7b08b6b40f9ef09cdbe787c2'
manifest = json.loads(manifest_bytes)
for entry in provenance['copy_files']:
path = Path(entry['target'])
assert path.is_relative_to(CONTEXT), str(path)
data = path.read_bytes()
assert len(data) == entry['bytes'] and hashlib.sha256(data).hexdigest() == entry['sha256'], str(path)
evidence = {'manifest_sha256': digest, 'verified_copy_files': len(provenance['copy_files']), 'members': {}, 'packages': {}}
for package, members in manifest['original_packages'].items():
for member, pin in members.items():
path = CONTEXT / 'saves' / package / member
data = path.read_bytes()
assert len(data) == pin['bytes'] and hashlib.sha256(data).hexdigest() == pin['sha256'], str(path)
evidence['members'][f'{package}/{member}'] = {'bytes': len(data), 'sha256': pin['sha256']}
world = (CONTEXT / 'saves' / package / 'world.bin').read_bytes()
magic, version, size = struct.unpack('<4sIQ', world[:16])
assert magic == b'TTWB' and version == 1
save_manifest = json.loads((CONTEXT / 'saves' / package / 'manifest.json').read_bytes())
assert hashlib.sha256(world[16:]).hexdigest() == save_manifest['checksum_world']
raw = subprocess.run(['zstd', '-d', '-q', '-c'], input=world[16:], check=True, capture_output=True).stdout
assert len(raw) == size
reader = VariantReader(raw)
snapshot = reader.read()
assert reader.offset == len(raw)
relationships = snapshot['systems']['runtime']['relationships']
evidence['packages'][package] = {'world_bytes': len(world), 'raw_bytes': size, 'relationship_payload': relationships,
'collection_present': 'relationships' in relationships, 'collection_type': type(relationships.get('relationships')).__name__,
'collection_count': len(relationships['relationships']), 'metric_shield_day_types': 'not observed: empty relationship collection'}
return evidence
if __name__ == '__main__':
print(json.dumps(inspect(), indent=2))
@@ -0,0 +1 @@
{"roster": ["tests/s4/test_original_relationship_model.gd", "tests/s4/test_original_relationship_runtime.gd", "tests/s4/test_relationship_event_service.gd", "tests/s4/test_runtime_relationship_events.gd", "tests/s6/test_runtime_snapshot_serialization.gd", "tests/s6/test_runtime_save_lifecycle.gd", "tests/ui/test_staff_management.gd"], "source_files": 1311, "source_bytes": 67319279, "free_bytes": 3396403200, "conservative_required_bytes": 381892079, "capacity_available": true, "parse_paths": 308, "future_native_phases": 9, "actual_checks": null, "engines": 0, "stages": 0, "formal_acceptance": false}
@@ -0,0 +1,158 @@
{
"manifest_sha256": "d07b6c42ecca11bf3557ee5034ce9cc70b5a750f7b08b6b40f9ef09cdbe787c2",
"verified_copy_files": 28,
"members": {
"initial/journal.log": {
"bytes": 35,
"sha256": "1ec35136aeb728392a832cd3b9197b270f96779cb09c7fe9c51ed385e30f158d"
},
"initial/world.bin": {
"bytes": 7523,
"sha256": "df91b4624c9afdeea2d366ff38d9f545fcff3f5f99223e3c04ae66d04b2f9fab"
},
"initial/manifest.json": {
"bytes": 420,
"sha256": "e9c4f7a8ea4ce9897c1e66c7861779fcd558f2dc314e08dbaef1b59bd145f419"
},
"initial/thumbnail.png": {
"bytes": 83,
"sha256": "0ef55674eca98b6748ae5ccca85a14b042c1545a143810c9c47b487119895c32"
},
"operating/journal.log": {
"bytes": 35,
"sha256": "2c7f851e72867d6d44247baa7624af46d22abaa31cb16dff197e8e5914d1e866"
},
"operating/world.bin": {
"bytes": 48631,
"sha256": "93732bb0bc042f4da0f90879a03dc3f3e374b3b45000840b0cbae142c42601c6"
},
"operating/manifest.json": {
"bytes": 422,
"sha256": "9c8b2c813c78ea1e19f43af31e77ff1547d707ca93f0d142c9e0df5e9bee8bd6"
},
"operating/thumbnail.png": {
"bytes": 83,
"sha256": "0ef55674eca98b6748ae5ccca85a14b042c1545a143810c9c47b487119895c32"
},
"moving/journal.log": {
"bytes": 35,
"sha256": "5f5f1cbb5a4b1c94945f503ae9c62989f628aa1db498974e3d5c3fe04cb89120"
},
"moving/world.bin": {
"bytes": 27413,
"sha256": "a577b190cb3b18cfc5cca1ef15d26521ea950bf5f3d3da68d19b1c8f43672158"
},
"moving/manifest.json": {
"bytes": 416,
"sha256": "52e87c19e39d7e4e922b47ef5979f4fecbe3211bd3f193ab30fd5418fffc5c73"
},
"moving/thumbnail.png": {
"bytes": 83,
"sha256": "0ef55674eca98b6748ae5ccca85a14b042c1545a143810c9c47b487119895c32"
},
"date-boundary/journal.log": {
"bytes": 35,
"sha256": "6fe4347f04e624f74a77d0eb85738508f653103e7522a0d37ed811519b5fe34f"
},
"date-boundary/world.bin": {
"bytes": 26686,
"sha256": "d4443f0bd42c2e9d6a5dd07d1949deade5d7526036f1320b56336b5637c9fcc3"
},
"date-boundary/manifest.json": {
"bytes": 430,
"sha256": "5e604491f863537d390f9db11f9dcc2473fcbf7fc0bced8d326caae893572ea2"
},
"date-boundary/thumbnail.png": {
"bytes": 83,
"sha256": "0ef55674eca98b6748ae5ccca85a14b042c1545a143810c9c47b487119895c32"
},
"defense/journal.log": {
"bytes": 35,
"sha256": "1ec35136aeb728392a832cd3b9197b270f96779cb09c7fe9c51ed385e30f158d"
},
"defense/world.bin": {
"bytes": 10110,
"sha256": "34ab140d64743b89425c292b785827fdb82c2bf92510688a4038461a2fe7a5d7"
},
"defense/manifest.json": {
"bytes": 418,
"sha256": "91d209a9bc295eadfa761359670a0c53ec7799a1f2ab53eedaacc14b2430b015"
},
"defense/thumbnail.png": {
"bytes": 83,
"sha256": "0ef55674eca98b6748ae5ccca85a14b042c1545a143810c9c47b487119895c32"
}
},
"packages": {
"initial": {
"world_bytes": 7523,
"raw_bytes": 61132,
"relationship_payload": {
"version": 1,
"relationships": [],
"threshold_candidates": [],
"next_candidate_sequence": 1
},
"collection_present": true,
"collection_type": "list",
"collection_count": 0,
"metric_shield_day_types": "not observed: empty relationship collection"
},
"operating": {
"world_bytes": 48631,
"raw_bytes": 1020364,
"relationship_payload": {
"version": 1,
"relationships": [],
"threshold_candidates": [],
"next_candidate_sequence": 1
},
"collection_present": true,
"collection_type": "list",
"collection_count": 0,
"metric_shield_day_types": "not observed: empty relationship collection"
},
"moving": {
"world_bytes": 27413,
"raw_bytes": 390624,
"relationship_payload": {
"version": 1,
"relationships": [],
"threshold_candidates": [],
"next_candidate_sequence": 1
},
"collection_present": true,
"collection_type": "list",
"collection_count": 0,
"metric_shield_day_types": "not observed: empty relationship collection"
},
"date-boundary": {
"world_bytes": 26686,
"raw_bytes": 579376,
"relationship_payload": {
"version": 1,
"relationships": [],
"threshold_candidates": [],
"next_candidate_sequence": 1
},
"collection_present": true,
"collection_type": "list",
"collection_count": 0,
"metric_shield_day_types": "not observed: empty relationship collection"
},
"defense": {
"world_bytes": 10110,
"raw_bytes": 78904,
"relationship_payload": {
"version": 1,
"relationships": [],
"threshold_candidates": [],
"next_candidate_sequence": 1
},
"collection_present": true,
"collection_type": "list",
"collection_count": 0,
"metric_shield_day_types": "not observed: empty relationship collection"
}
}
}
@@ -0,0 +1,22 @@
{
"roster": [
"tests/s4/test_original_relationship_model.gd",
"tests/s4/test_original_relationship_runtime.gd",
"tests/s4/test_relationship_event_service.gd",
"tests/s4/test_runtime_relationship_events.gd",
"tests/s6/test_runtime_snapshot_serialization.gd",
"tests/s6/test_runtime_save_lifecycle.gd",
"tests/ui/test_staff_management.gd"
],
"source_files": 1311,
"source_bytes": 67498284,
"free_bytes": 3390361600,
"conservative_required_bytes": 382071084,
"capacity_available": true,
"parse_paths": 308,
"future_native_phases": 9,
"actual_checks": null,
"engines": 0,
"stages": 0,
"formal_acceptance": false
}
@@ -0,0 +1,11 @@
{
"autoload/simulation_runtime.gd": "0c8c9356a78bf9df16c362e202a362e044bae018f40efb54145713582d69a2d2",
"docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json": "ea74b503b41d7a38f0ee26f8a7d03626edd7d941329d2355ce235d65d2bf4855",
"docs/plans/original-completion-2026-10-02/execution/p18-01-code/IMPLEMENTATION.md": "28fa682dcc4f6b33347d6e10046d093f3bfb43bae04bb3411d368885d53cc8bc",
"scripts/relationship/relationship_service.gd": "2d137a4dc3712356434b2609723b88292fb717625241e07f9e60984d830df80a",
"tests/s4/test_original_relationship_model.gd": "c74b243dbbb54dba9bbc165e29508cca910f0beb63e32d25ebbb8ea606dccce9",
"tests/s4/test_original_relationship_runtime.gd": "349271cc8b8a4750ef339ecac6905852bd7a5d006cca68ee10d5d0c737f426de",
"tools/regression/suite-policy.json": "2145ef977e72e490d27fddd550c0c00827b52a2e0b844d60e6f86df9a666d5ae",
"tools/run_p18_relationship_tests.py": "ccc8976dd9e9863366d102719dc0ae21d3a57da45dec39b4c72a3fc8c9f31a9b",
"docs/plans/original-completion-2026-10-02/execution/p18-01-code/repair-4-prepare.json": "9d0ae1b3f1e91d44ac84a2a55388bc56e96637dec925f42ecc9757de1c3a3a28"
}
+7 -6
View File
@@ -543,7 +543,7 @@ func _parse_snapshot(snapshot: Dictionary) -> Dictionary:
static func _legacy_types_valid(raw: Dictionary) -> bool: static func _legacy_types_valid(raw: Dictionary) -> bool:
for key: String in ["relationship_id", "actor_a_id", "actor_b_id"]: for key: String in ["relationship_id", "actor_a_id", "actor_b_id"]:
if not (raw.get(key) is String or raw.get(key) is StringName): if not raw.get(key) is String:
return false return false
for key: String in ["shield_max", "shield_current", "last_event_at", "daily_attack_count", "attack_day", "metric_delta_day"]: for key: String in ["shield_max", "shield_current", "last_event_at", "daily_attack_count", "attack_day", "metric_delta_day"]:
if raw.has(key) and not raw[key] is int: if raw.has(key) and not raw[key] is int:
@@ -559,9 +559,9 @@ static func _legacy_types_valid(raw: Dictionary) -> bool:
for value: Variant in raw.get(key, {}).values(): for value: Variant in raw.get(key, {}).values():
if not value is int: if not value is int:
return false return false
for key: String in ["action_counts", "flags"]: for key: String in ["metrics", "action_counts", "daily_metric_deltas", "flags", "crossed_thresholds"]:
for identity: Variant in raw.get(key, {}): for identity: Variant in raw.get(key, {}):
if not (identity is String or identity is StringName): if not identity is String:
return false return false
for value: Variant in raw.get("flags", {}).values(): for value: Variant in raw.get("flags", {}).values():
if not value is bool: if not value is bool:
@@ -572,10 +572,11 @@ static func _legacy_types_valid(raw: Dictionary) -> bool:
for threshold: Variant in value: for threshold: Variant in value:
if not threshold is int: if not threshold is int:
return false return false
if raw.has("route_id") and not (raw["route_id"] is String or raw["route_id"] is StringName): if raw.has("route_id") and not raw["route_id"] is String:
return false
if raw.has("route_lock") and not (raw["route_lock"] is int or raw["route_lock"] is String or raw["route_lock"] is StringName):
return false return false
if raw.has("route_lock"):
if not raw["route_lock"] is String or not RelationshipStateType.RouteLock.keys().has(raw["route_lock"]):
return false
return true return true
@@ -30,7 +30,50 @@ func _service() -> RelationshipService:
) )
static func legacy_fixture() -> Dictionary:
return {"version": 1, "relationships": [
{"relationship_id": "aliases", "actor_a_id": "heroine_anri", "actor_b_id": "player",
"metrics": {"affinity": 41, "obligation": 19, "extension": {"opaque": [3, "keep"]}},
"extra": ["raw", 2.5]},
{"relationship_id": "second", "actor_a_id": "departed", "actor_b_id": "player",
"metrics": {"trust": 7}}],
"threshold_candidates": [], "next_candidate_sequence": 1, "extension": ["ordered", 7]}
func _alias_mutations() -> void:
var service := _service()
var legacy := legacy_fixture()
var input := var_to_bytes(legacy)
_expect(service.restore_snapshot(legacy), "int v1 aliases accepted")
_expect(service.apply_value_change(&"aliases", &"affection", 2, 0).is_success(), "affection mutation through real service")
_expect(service.apply_value_change(&"aliases", &"dependence", 3, 0).is_success(), "dependence mutation through real service")
var saved := service.create_snapshot()
var raw: Dictionary = saved[&"legacy"]["relationships"][0]
_expect(raw["metrics"]["affection"] == 43 and raw["metrics"]["dependence"] == 22, "canonical changes merged")
_expect(raw["metrics"]["affinity"] == 41 and raw["metrics"]["obligation"] == 19, "aliases remain old facts")
_expect(raw["metrics"]["extension"] == legacy["relationships"][0]["metrics"]["extension"] and raw["extra"] == ["raw", 2.5], "unknown facts survive mutation")
_expect(saved[&"legacy"]["relationships"][1] == legacy["relationships"][1], "record order preserved")
var restored := _service()
_expect(restored.restore_snapshot(saved), "mutated v2 restores")
_expect(var_to_bytes(restored.create_snapshot()) == var_to_bytes(saved), "mutated v2 raw order and bytes preserved")
_expect(restored.get_relationship(&"aliases").get_metric(&"affection") == 43 and restored.get_relationship(&"aliases").get_metric(&"dependence") == 22, "canonical values take precedence over raw aliases")
for key: StringName in [&"affection", &"lust", &"netorare"]:
_expect(restored.get_original_relationship(&"heroine_anri")[key] == null, "old axes never configure original " + String(key))
_expect(var_to_bytes(legacy) == input, "mutation never changes input bytes")
for value: Variant in ["50", null, true, 50.0, 1.5, INF, NAN]:
var bad := legacy_fixture()
bad["relationships"][0]["metrics"]["affinity"] = value
_rejected(restored, bad)
var bad := legacy_fixture()
bad["relationships"][0]["metrics"] = []
_rejected(restored, bad)
bad = saved.duplicate(true)
bad[&"relationships"] = []
_rejected(restored, bad)
func _run() -> void: func _run() -> void:
_alias_mutations()
var service := _service() var service := _service()
var before := var_to_bytes(service.create_snapshot()) var before := var_to_bytes(service.create_snapshot())
var unset := service.get_original_relationship(&"heroine_anri") var unset := service.get_original_relationship(&"heroine_anri")
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -2,14 +2,14 @@
"schema_version": 1, "schema_version": 1,
"suite_policies": { "suite_policies": {
"tests/s4/test_original_relationship_model.gd": { "tests/s4/test_original_relationship_model.gd": {
"source_sha256": "ad509f5fb67ab91e1d1d99a08af51e4695e411b9a2541ea63ee22b1555cb0924", "source_sha256": "c74b243dbbb54dba9bbc165e29508cca910f0beb63e32d25ebbb8ea606dccce9",
"ordinary_cleanup_prepared": false, "ordinary_cleanup_prepared": false,
"ordinary_control_labels": [], "ordinary_control_labels": [],
"old_bounded_teardown": false, "old_bounded_teardown": false,
"new_audio_QA": false "new_audio_QA": false
}, },
"tests/s4/test_original_relationship_runtime.gd": { "tests/s4/test_original_relationship_runtime.gd": {
"source_sha256": "374f3d31a65d2f2b62dfef29721c8c838575b856f4fa5a0bca31c6ba4c7e09dd", "source_sha256": "845ae200575563400c3497e517137effb7d4d928dd7e6a4990ed8aabdbda5ae6",
"ordinary_cleanup_prepared": false, "ordinary_cleanup_prepared": false,
"ordinary_control_labels": [], "ordinary_control_labels": [],
"old_bounded_teardown": false, "old_bounded_teardown": false,
+10 -7
View File
@@ -199,6 +199,7 @@ def seal(out, refs, engine, contract_sha):
source = content_map(refs) source = content_map(refs)
shared.write(out / 'receipt.json', { shared.write(out / 'receipt.json', {
'schema': 'guarded-p18-v2', 'source_map': source, 'schema': 'guarded-p18-v2', 'source_map': source,
'parse_checks': len([name for name in refs if name.endswith('.gd')]),
'source_map_sha256': digest(source), 'source_map_sha256': digest(source),
'project_settings_sha256': source['project.godot']['sha256'], 'project_settings_sha256': source['project.godot']['sha256'],
'marker_contract_sha256': contract_sha, 'marker_contract_sha256': contract_sha,
@@ -281,8 +282,10 @@ def verify(out, refs, markers, contracts, policy, contract_sha, parse_paths):
require(engine and shared.sha(engine) == start['engine_sha256'] require(engine and shared.sha(engine) == start['engine_sha256']
== manifest['engine_sha256'] == files['engine.bin']['sha256'], == manifest['engine_sha256'] == files['engine.bin']['sha256'],
'current native engine and retained bytes pinned') 'current native engine and retained bytes pinned')
require(read('parse-paths.json') == parse_paths and len(parse_paths) == 308, derived_paths = ['res://' + name for name in sorted(refs) if name.endswith('.gd')]
'all 308 exact source parse paths') require(read('parse-paths.json') == parse_paths == derived_paths
and manifest['parse_checks'] == len(derived_paths),
'all exact source parse paths and receipt count')
require(end['passed'] is True and end['engine_ended'] is True and end['error'] is None require(end['passed'] is True and end['engine_ended'] is True and end['error'] is None
and end['cleanup_errors'] == [] and end['guard_errors'] == [], and end['cleanup_errors'] == [] and end['guard_errors'] == [],
'finished successful collection without guard/cleanup error') 'finished successful collection without guard/cleanup error')
@@ -463,8 +466,9 @@ def verify(out, refs, markers, contracts, policy, contract_sha, parse_paths):
else: else:
require(row['controlled_environment'] == {}, 'fresh import environment') require(row['controlled_environment'] == {}, 'fresh import environment')
if phase == 'parse': if phase == 'parse':
require(re.findall(r'(?m)^FULL_DEPENDENCY_PARSE checks=([0-9]+) failures=([0-9]+)\n', raw) require(len(re.findall(r'(?m)^FULL_DEPENDENCY_PARSE\b', raw)) == 1
== [('308', '0')] and row['actual_parse_checks'] == 308, 'unique all-script parse marker') and re.findall(r'(?m)^FULL_DEPENDENCY_PARSE checks=([0-9]+) failures=([0-9]+)\n', raw)
== [(str(len(parse_paths)), '0')] and row['actual_parse_checks'] == len(parse_paths), 'unique all-script parse marker')
if test: if test:
parsed = markers.parse_raw(test, raw, contracts, source[test]['sha256']) parsed = markers.parse_raw(test, raw, contracts, source[test]['sha256'])
require(parsed['passed'] and row['completion'] == parsed require(parsed['passed'] and row['completion'] == parsed
@@ -492,7 +496,7 @@ def verify(out, refs, markers, contracts, policy, contract_sha, parse_paths):
'whole raw evidence unchanged during verification') 'whole raw evidence unchanged during verification')
require(content_map(shared.source_refs(shared.HERE.parent)) == source, require(content_map(shared.source_refs(shared.HERE.parent)) == source,
'whole current source unchanged during verification') 'whole current source unchanged during verification')
print(json.dumps({'verified': True, 'actual_suites': 7, 'parse_checks': 308, print(json.dumps({'verified': True, 'actual_suites': 7, 'parse_checks': len(parse_paths),
'actual_checks': end['actual_all_roster_checks'], 'engines_started': 0, 'actual_checks': end['actual_all_roster_checks'], 'engines_started': 0,
'formal_acceptance': False, 'original_unit_complete': False})) 'formal_acceptance': False, 'original_unit_complete': False}))
return 0 return 0
@@ -536,7 +540,6 @@ def main(argv=None):
require(contracts[test]['source_sha256_at_schema_audit'] == refs[test]['sha256'], require(contracts[test]['source_sha256_at_schema_audit'] == refs[test]['sha256'],
'new strict source-hashed marker') 'new strict source-hashed marker')
parse_paths = ['res://' + name for name in sorted(refs) if name.endswith('.gd')] parse_paths = ['res://' + name for name in sorted(refs) if name.endswith('.gd')]
require(len(parse_paths) == 308, 'exact expected parse count')
if args.prepare_only: if args.prepare_only:
ancestor = out.parent ancestor = out.parent
while not ancestor.exists(): while not ancestor.exists():
@@ -545,7 +548,7 @@ def main(argv=None):
size = sum(p['bytes'] for p in refs.values()) size = sum(p['bytes'] for p in refs.values())
print(json.dumps({'roster': ROSTER, 'source_files': len(refs), 'source_bytes': size, print(json.dumps({'roster': ROSTER, 'source_files': len(refs), 'source_bytes': size,
'free_bytes': free, 'conservative_required_bytes': size + shared.MIN_SPACE, 'free_bytes': free, 'conservative_required_bytes': size + shared.MIN_SPACE,
'capacity_available': free >= size + shared.MIN_SPACE, 'parse_paths': 308, 'capacity_available': free >= size + shared.MIN_SPACE, 'parse_paths': len(parse_paths),
'future_native_phases': 9, 'actual_checks': None, 'engines': 0, 'stages': 0, 'future_native_phases': 9, 'actual_checks': None, 'engines': 0, 'stages': 0,
'formal_acceptance': False})) 'formal_acceptance': False}))
return 0 return 0