Merge commit '265ce30f81b1302ca374330f7129f02d6fde4697' into HEAD

This commit is contained in:
Agent Control Runner
2026-10-15 23:13:10 +09:00
19 changed files with 3436 additions and 11 deletions
+32
View File
@@ -330,6 +330,7 @@ func initialize(clock: SimulationKernel, state: Variant, seed := DEFAULT_WORLD_S
register_command_executor(&"begin_staff_task", Callable(self, "_execute_begin_staff_task"))
register_command_executor(&"progress_staff_task", Callable(self, "_execute_progress_staff_task"))
register_command_executor(&"create_relationship", Callable(self, "_execute_create_relationship"))
register_command_executor(&"register_original_relationship", Callable(self, "_execute_register_original_relationship"))
register_command_executor(&"change_relationship", Callable(self, "_execute_change_relationship"))
register_command_executor(&"attack_relationship_shield", Callable(self, "_execute_attack_relationship_shield"))
register_command_executor(&"request_route_lock", Callable(self, "_execute_request_route_lock"))
@@ -1054,6 +1055,9 @@ func _restore_snapshot_internal(snapshot: Dictionary, recovery_mode: bool) -> bo
):
return false
var restored_relationships := RelationshipService.new()
restored_relationships.configure_original_identity_validator(
func(payload: Dictionary) -> bool: return _original_relationship_identities_valid(payload, restored_staff, restored_catalog)
)
if not restored_relationships.restore_snapshot(runtime_data[&"relationships"]):
return false
var restored_events := EventService.new()
@@ -5378,6 +5382,9 @@ func _initialize_s4_relationship_events() -> void:
func _configure_relationship_event_boundaries() -> void:
if relationship_service == null or event_service == null:
return
relationship_service.configure_original_identity_validator(
func(payload: Dictionary) -> bool: return _original_relationship_identities_valid(payload, staff_service, mvp_content_catalog)
)
relationship_service.configure_candidate_callback(
Callable(self, "_on_relationship_threshold_candidate")
)
@@ -6759,6 +6766,30 @@ func _execute_progress_staff_task(command: SimulationCommand) -> CommandResult:
result.command_id = command.command_id
return result
func _original_relationship_identities_valid(payload: Dictionary, staff_registry: StaffService, catalog: MvpContentCatalog) -> bool:
if payload.get("protagonist_id") != "player" or staff_registry == null:
return false
var subject := StringName(payload.get("subject_id", ""))
var rival := StringName(payload.get("rival_id", ""))
if subject == &"player" or rival == &"player" or subject == rival or not staff_registry.has_staff(subject):
return false
var heroine_definition := catalog.get_entry(&"unique_characters", subject)
var rival_definition := catalog.get_entry(&"unique_characters", rival)
var subject_staff := staff_registry.get_staff(subject)
return subject_staff.employment_kind == StaffRuntime.EmploymentKind.UNIQUE \
and heroine_definition.get(&"role_id") == String(subject_staff.role_id) \
and heroine_definition.get(&"character_kind") == "heroine" \
and rival_definition.get(&"character_kind") == "rival" \
and heroine_definition.get(&"counterpart_id") == String(rival) \
and rival_definition.get(&"counterpart_id") == String(subject)
func _execute_register_original_relationship(command: SimulationCommand) -> CommandResult:
var result := relationship_service.register_original_relationship(command.payload)
result.command_id = command.command_id
return result
func _execute_create_relationship(command: SimulationCommand) -> CommandResult:
var actor_a_id := StringName(command.payload.get(&"actor_a_id", command.payload.get("actor_a_id", "")))
var actor_b_id := StringName(command.payload.get(&"actor_b_id", command.payload.get("actor_b_id", "")))
@@ -10889,6 +10920,7 @@ func get_staff_management_report() -> Dictionary:
&"can_edit": block_reason.is_empty(),
&"assignment_block_reason": block_reason,
&"unique_contract": _staff_unique_contract_report(staff),
&"original_relationship": relationship_service.get_original_relationship(staff.staff_id),
})
var facilities: Array[Dictionary] = []
for facility: FacilityInstance in facility_service.get_instances():
@@ -2,6 +2,28 @@
"schema_version": 1,
"scope": "exact per-suite completion label/schema; all check counts from actual raw only",
"rows": [
{
"test": "tests/s4/test_original_relationship_model.gd",
"template": "Original relationship model: <checks> checks, <failures> failures",
"required": true,
"fixed_check_count_used": false,
"source_sha256_at_schema_audit": "c74b243dbbb54dba9bbc165e29508cca910f0beb63e32d25ebbb8ea606dccce9",
"authority": {
"path": "docs/spec/acceptance/P18-01-code-r01.json",
"scope": "new backend suite marker; native execution pending"
}
},
{
"test": "tests/s4/test_original_relationship_runtime.gd",
"template": "Original relationship runtime: <checks> checks, <failures> failures",
"required": true,
"fixed_check_count_used": false,
"source_sha256_at_schema_audit": "c56bdb99dd43a4e7ceb3ca2861e8db39972c5c4e805126a77e2f7da32ca5bdc9",
"authority": {
"path": "docs/spec/acceptance/P18-01-code-r01.json",
"scope": "new backend suite marker; native execution pending"
}
},
{
"test": "tests/ui/test_selection_departure.gd",
"template": "Selection departure: <checks> checks, <failures> failures",
@@ -0,0 +1,356 @@
# P18-01-CODE implementation handoff
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,
Runtime registration and read projection, and the contract-listed tests and collector.
Production initial values and gameplay coefficients are not supplied or inferred.
Validation: worker static inspection only; authoritative native local and affected
validation, independent review, and integration belong to Runner.
Source inspection: HEAD matches d2a1f80a613e7c77b02b941026bc49c2d85db863.
The acceptance specification without its final newline matches the contract SHA256
04e9ae701b6ee1e399542564add695ca7fce54499e52dd2e907b1dbdc23a366d.
## Submitted behavior
- OriginalRelationshipState stores explicit directional subject/protagonist/rival
facts with Affection, Lust, Netorare and shield current/max. All fields are
required for a configured record. Numeric inputs must be finite numbers;
shield current must be nonnegative and at most max, and max equals Affection.
No additional emotion ranges, coefficients or production values are imposed.
- RelationshipService retains its five-axis API. Its v2 snapshot contains the
whole v1 payload under `legacy`, plus separate `original_relationships`.
Top-level `relationships` is a compatibility projection for existing consumers;
disagreement with `legacy.relationships` is rejected. Additional legacy fields,
raw aliases, collection order and untouched values survive migration. The
existing normalized state remains usable; only actual legacy changes are merged
into retained raw facts. Aliases never initialize original fields.
- Registration uses `register_original_relationship` with the complete payload.
Runtime validates the protagonist against `player`, the subject against hired
UNIQUE staff and its canonical role, and heroine/rival identities and reciprocal
counterpart references against the existing unique-character catalog. Rival
catalog membership does not invent recruitment. No new original update policy,
contact, route, attack, threshold or duel behavior is defined.
- Snapshot restore validates candidates before replacing live services, then
rebinds the identity validator to live staff. Legacy relationships retain the original acceptance of departed guests; original
records separately validate staff/person/catalog references.
- Every staff-management row exposes `original_relationship`. Missing records
have `configured: false` and null values, distinct from explicit zero. Configured
projections are copies, so reads and caller edits cannot change service state.
## Validation and handoff
Two new s4 suites exercise explicit/unset/zero data, malformed and nonfinite values,
identity invariants, lossless raw aliases/extensions, new and mixed round trips,
atomic service/Runtime rejection, actual hiring/commands, restore/rebind and reports.
Existing test bodies and limits are unchanged. New marker and suite-policy rows
are pinned to the exact new test sources.
`python3 -B tools/run_p18_relationship_tests.py --prepare-only` previews the exact
seven-suite roster, source bytes, capacity and all-script parse count without
engines, profiles, stage creation or test execution. The native collector reuses
run_all_tests.source_refs/execute_phase and the existing Guard, owned_outer,
diagnostic, artifact, PID, marker and cleanup gates. It stops at the first failure;
Runner owns repair/validation sequencing and independent review.
Worker checks: Python AST syntax, JSON/source-marker consistency, allowed-path
inspection, unchanged pre-existing marker/policy records, and git diff whitespace.
Native import, GDScript parse and all seven suites remain **unexecuted** by worker.
Worker reinspection on 2026-10-13 confirmed the existing submitted implementation
and repeated Python AST, specification digest, exact new marker/policy source
hashes, unchanged existing marker/policy records, and `git diff --check`.
Prepare-only exited 0: seven suites, 1,311 source files / 67,287,056 bytes,
308 parse paths, nine future native phases, 381,859,856 conservative required
bytes and 1,773,187,072 available bytes. Engines and stages were both zero;
actual checks were null and formal acceptance was false. Capacity is a preview
at that instant, not a reservation or native result.
An initial shell here-document temporary-file attempt was denied with
`operation not permitted`; normal allowed-path patch edits worked. No isolation
rule or permission was relaxed.
P18-C-1 through P18-C-3 are implementation submissions, not accepted results.
P18-C-4 awaits Runner native local/affected evidence, separate Opus review,
integration and final affected validation. The original P18-01 unit remains
in_progress: ordinary Japanese UI rendering/equality, approved production values,
all-seven recruitment, final full required roster and applicable Windows final
acceptance remain outstanding. No git writes, other agents or Runner controls
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.
### Repair 6 — actual Opus high/medium feedback
Preserved the held implementation and all representative inputs. Added strict
candidate field validation matching `_register_threshold_candidate`: identifiers
and actor identities accept String/StringName, deduplication_key is String,
threshold/created_at are int, requires_unlock_event is bool, and actor_ids is
an Array of two identities. No numeric bounds or gameplay policy were added.
Unknown candidate fields remain raw facts.
The new Runtime suite now keeps nested legacy and compatibility projection
identical for the exclusive-route positive control and empty-route-only negative
control. Both exercise normal restore and real checksum-valid SaveService package
recovery. Negative checks retain whole Runtime bytes, service identity/bytes,
caller bytes, RESTORE_FAILED, restore_committed=false and no forbidden repair.
Producer-created typed candidates traverse v1, v2 and package recovery; String
identifier compatibility and unknown raw extensions have positive controls.
Each reviewed wrong field type exercises service, normal Runtime and recovery
rejection for both v1 and v2. Native execution remains pending Root ownership.
Ordering disposition: preserve restored raw collection order under the explicit
lossless round-trip contract. Fresh snapshots retain their existing sorted output.
The actual producer issues zero-padded monotonic ids; after restore, new candidates
append without disturbing raw facts. Added a real producer mutation check for
that boundary. No new sorting policy was introduced. The unrelated absolute-path
historical inspector limitation remains deferred.
Static diff whitespace checks passed. All five existing suites retain the exact
SHA-256 values forwarded by Root. Both new test pins remain exact (model unchanged;
Runtime c56bdb99dd43a4e7ceb3ca2861e8db39972c5c4e805126a77e2f7da32ca5bdc9).
Prepare-only: seven suites, 308 parse paths, 1,311 source files, zero engines/stages,
no executed checks and formal_acceptance=false. No native/default-profile/full
regression, git writes, Runner operations, integration or agents were used.
P18-01 remains 30/88, in progress; submitted for Root verification, not accepted.
The final repair-6-prepare.json receipt records 67,504,813 source bytes,
2,674,012,160 free bytes and 382,077,613 single-set admission bytes. Two sets
at the retained conservative 474 MiB estimate require 994,050,048 bytes and fit
at this preview. Capacity is not reserved and must be checked at collection time.
@@ -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"
}
@@ -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": 67504813, "free_bytes": 2674012160, "conservative_required_bytes": 382077613, "capacity_available": true, "parse_paths": 308, "future_native_phases": 9, "actual_checks": null, "engines": 0, "stages": 0, "formal_acceptance": false}
@@ -0,0 +1,80 @@
# P18-01-CODE recorded repair 2/2: collection / verification split
Resumes candidate `f76ad96990a4d7371b6ab667df0a068cc06be798`. No counter reset,
game/test reconstruction, Runner change, commit or integration. Changes are confined
to the narrow collector and this note. Existing game behavior evidence remains the
retained Astra preflight; this worker did not rerun or reinterpret it as final proof.
## Modes
- Default: `python -B tools/run_p18_relationship_tests.py --output {build_dir}/p18-relationship-evidence`.
Read-only verification in the ordinary registered sandbox. No subprocess call,
process discovery, native launch, or access to the external player/private profiles.
Current checkout source/settings/support/marker bytes and the locally configured
`godot` binary are read for pins. Receipt-provided paths never select an external
file to open. Original absolute paths remain historical identity strings.
- `--collect`: Root orchestration only, outside Seatbelt. Uses the existing
`source_refs`, private source copy, `execute_phase`, owned_outer, Guard, logger,
diagnostic, marker and bounded cleanup contracts unchanged. Fresh output outside
the checkout, with an existing parent, is required. Runs import, all 308 scripts,
and exactly the seven fixed tests once. Owned profile bytes are archived after
each phase. The final verifier runs after collection; no duplicate regression.
“Root” identifies the authorized orchestrator, not a requirement to run as Unix uid 0.
- `--prepare-only`: reports source bytes, available capacity, 308 parse paths and
the exact seven-test/nine-phase roster; creates no stage/profile and starts no engine.
Only `--output`, `--collect`, `--prepare-only` and help are accepted. Duplicate,
abbreviated, unknown and malformed flags, combined modes, traversal, symlinks,
nonregular evidence and hardlinked evidence reject. Verification never rewrites
raw paths or files. The ordinary-owner pure proof reader maps only the original
output prefix to the supplied output and checks confinement before opening files.
## Receipt schema
`receipt.json` (`guarded-p18-v2`) is written last after successful collection.
It contains the full relative source content map (bytes/SHA-256, no checkout-path
dependency), its canonical JSON SHA-256, project and marker-contract SHA-256,
engine SHA-256, and a complete relative file inventory with byte sizes/SHA-256.
The inventory covers the retained private source, engine bytes, bootstraps,
starters, raw stdout/stderr/native logger, phase receipts, START/END/process and
both cleanup records, Guard reports, ordinary/audio ownership proofs, typed
snapshots, and complete archived startup/runtime profiles. Only the enclosing
receipt itself is outside its own inventory.
Verification checks the inventory and recomputes raw marker/diagnostic gates,
source and helper pins, exact commands, START/END identity, all nine distinct PIDs
and bounded cleanup, Guard conditions, proof gates, complete player before/after
inventory equality, and seven test rows against nine phase receipts. It requires
one exact `FULL_DEPENDENCY_PARSE checks=308 failures=0` marker. Old v1, absent,
incomplete, mismatched or altered evidence rejects. A source change, including
this collector, requires fresh Root collection; no source exemptions exist.
## Worker checks and environment observations
AST/in-memory compile and prepare-only passed. Prepare-only found 1311 source
files, 308 parse scripts, seven tests and nine future phases, with sufficient
reported capacity. Synthetic negative checks rejected missing receipts, changed
hashes, symlinks, relative and absolute escapes, incomplete files and invalid or
duplicate flags/mode combinations. Those checks replaced subprocess entry points
and default-player access with fail-fast functions; neither was called. No native
test, full regression, `--collect`, Runner operation or review was executed.
Environment evidence: a shell here-document failed with
`zsh:1: can't create temp file for here document: operation not permitted`.
The `python` alias was unavailable (`zsh:1: command not found: python`);
worker checks used `/usr/bin/python3 -B`. Ordinary authorized file edits and
temporary negative fixtures under the task allocation succeeded. No isolation
or permission policy was changed. Root/Runner must provide the configured
`python` command used by the fixed validator argv.
## Root next steps
1. On the final unchanged source, run the native `--collect` exactly once outside
Seatbelt into the allocation build directory's fresh `p18-relationship-evidence`.
Do not reuse old v1 evidence or run collection from ordinary Runner validation.
2. Run ordinary Runner validation using the fixed default verify-only argv.
3. Obtain a separate Opus review, then perform the authorized integration and
affected final validation. This worker submits implementation, not acceptance.
P18-01 remains incomplete: ordinary UI rendering/display equality, approved initial
values, the full final roster and applicable Windows final acceptance remain unmet.
@@ -0,0 +1,38 @@
class_name OriginalRelationshipState
extends RefCounted
## Explicit facts only. Numeric values are not production initialization approval.
## No update, attack, reduction, threshold or route policy is defined here.
const FIELDS := ["subject_id", "protagonist_id", "rival_id", "affection", "lust", "netorare", "shield_current", "shield_max"]
var _payload: Dictionary = {}
static func from_snapshot(value: Variant) -> OriginalRelationshipState:
if not value is Dictionary or value.size() != FIELDS.size():
return null
for field: String in FIELDS:
if not value.has(field):
return null
for field: String in ["subject_id", "protagonist_id", "rival_id"]:
if not (value[field] is String or value[field] is StringName) or String(value[field]).is_empty():
return null
if value["subject_id"] == value["protagonist_id"] or value["subject_id"] == value["rival_id"] or value["protagonist_id"] == value["rival_id"]:
return null
for field: String in ["affection", "lust", "netorare", "shield_current", "shield_max"]:
if not (value[field] is int or value[field] is float):
return null
if value[field] is float and not is_finite(value[field]):
return null
if value["shield_max"] != value["affection"] or value["shield_current"] < 0 or value["shield_current"] > value["shield_max"]:
return null
var state := OriginalRelationshipState.new()
state._payload = value.duplicate(true)
return state
func create_snapshot() -> Dictionary:
return _payload.duplicate(true)
func subject_id() -> StringName:
return StringName(_payload.get("subject_id", ""))
@@ -0,0 +1 @@
uid://cqxwpbkgjtnsa
+182 -11
View File
@@ -9,8 +9,9 @@ signal route_lock_changed(relationship_id: StringName, old_lock: int, new_lock:
const RelationshipStateType = preload("res://scripts/relationship/relationship_state.gd")
const CommandResultType = preload("res://scripts/core/command_result.gd")
const OriginalStateType = preload("res://scripts/relationship/original_relationship_state.gd")
const SNAPSHOT_VERSION := 1
const SNAPSHOT_VERSION := 2
const DAILY_AUTOMATIC_DELTA_LIMIT := 10
const GATED_THRESHOLDS: Array[int] = [50, 80]
const STAGE_THRESHOLDS: Array[int] = [20, 50, 80, 100]
@@ -22,6 +23,46 @@ var _pair_index: Dictionary = {}
var _threshold_candidates: Array[Dictionary] = []
var _next_candidate_sequence := 1
var _candidate_callback := Callable()
var _original_records: Dictionary = {}
var _original_identity_validator := Callable()
var _legacy_raw: Dictionary = {}
var _legacy_baselines: Dictionary = {}
var _container_raw: Dictionary = {}
func configure_original_identity_validator(callback: Callable) -> RelationshipService:
_original_identity_validator = callback
return self
func register_original_relationship(payload: Variant) -> CommandResult:
var state := OriginalStateType.from_snapshot(payload)
if state == null:
return _invalid("Original relationship requires complete explicit facts.")
if not _original_identity_validator.is_valid() or not bool(_original_identity_validator.call(state.create_snapshot())):
return _invalid("Original relationship identities are not registered.")
if _original_records.has(state.subject_id()):
return _duplicate("Original relationship subject", state.subject_id())
_original_records[state.subject_id()] = state.create_snapshot()
last_error = ""
return _success({&"subject_id": state.subject_id()})
func get_original_relationship(subject_id: StringName) -> Dictionary:
if _original_records.has(subject_id):
var result: Dictionary = _original_records[subject_id].duplicate(true)
result[&"configured"] = true
return result
return {&"configured": false, &"subject_id": subject_id,
&"protagonist_id": null, &"rival_id": null, &"affection": null,
&"lust": null, &"netorare": null, &"shield_current": null, &"shield_max": null}
func validate_original_identities() -> bool:
for payload: Dictionary in _original_records.values():
if not _original_identity_validator.is_valid() or not bool(_original_identity_validator.call(payload.duplicate(true))):
return false
return true
func configure_candidate_callback(callback: Callable = Callable()) -> RelationshipService:
@@ -329,6 +370,8 @@ func consume_threshold_candidate(candidate_id: StringName) -> Dictionary:
func validate_invariants() -> Dictionary:
var errors: Array[String] = []
if not validate_original_identities():
errors.append("Original relationship identity reference is invalid.")
var seen_pairs: Dictionary = {}
for relationship_id: StringName in _sorted_name_keys(_relationships):
var state := get_relationship(relationship_id)
@@ -347,22 +390,92 @@ func validate_invariants() -> Dictionary:
func create_snapshot() -> Dictionary:
var relationships: Array[Dictionary] = []
for state: RelationshipState in get_relationships():
relationships.append(state.create_snapshot())
var current := state.create_snapshot()
var original: Dictionary = _legacy_baselines.get(state.relationship_id, {})
var raw: Dictionary = {}
for entry: Dictionary in _legacy_raw.get(&"relationships", []):
if StringName(_value(entry, &"relationship_id", "")) == state.relationship_id:
raw = entry
break
relationships.append(_merge_legacy_changes(raw, original, current) if not original.is_empty() else current)
var candidates := _threshold_candidates.duplicate(true)
candidates.sort_custom(
func(a: Dictionary, b: Dictionary) -> bool:
return String(a.get(&"candidate_id", "")) < String(b.get(&"candidate_id", ""))
)
return {
&"version": SNAPSHOT_VERSION,
&"relationships": relationships,
&"threshold_candidates": candidates,
&"next_candidate_sequence": _next_candidate_sequence,
}
var legacy := _legacy_raw.duplicate(true)
legacy[&"version"] = 1
# Preserve input collection order as well as every untouched raw field.
if _legacy_raw.has(&"relationships"):
var ordered: Array[Dictionary] = []
for raw: Dictionary in _legacy_raw[&"relationships"]:
for entry: Dictionary in relationships:
if _value(entry, &"relationship_id", "") == _value(raw, &"relationship_id", ""):
ordered.append(entry)
for entry: Dictionary in relationships:
if not ordered.has(entry):
ordered.append(entry)
relationships = ordered
if legacy.get(&"relationships") != relationships:
legacy[&"relationships"] = relationships
var current_candidates := _threshold_candidates.duplicate(true) if not _legacy_raw.is_empty() else candidates
if legacy.get(&"threshold_candidates") != current_candidates:
legacy[&"threshold_candidates"] = current_candidates
legacy[&"next_candidate_sequence"] = _next_candidate_sequence
var result := _container_raw.duplicate(true)
result[&"version"] = SNAPSHOT_VERSION
result[&"legacy"] = legacy
# Compatibility projection for existing snapshot consumers; the nested v1
# record remains lossless, including extension names reserved by v2.
if result.get(&"relationships") != relationships:
result[&"relationships"] = relationships.duplicate(true)
var originals: Array[Dictionary] = []
for payload: Dictionary in _original_records.values():
originals.append(payload.duplicate(true))
if result.get(&"original_relationships") != originals:
result[&"original_relationships"] = originals
return result
static func _merge_legacy_changes(raw: Dictionary, baseline: Dictionary, current: Dictionary) -> Dictionary:
var result := raw.duplicate(true)
for key: Variant in baseline:
if not current.has(key):
result.erase(key)
for key: Variant in current:
if baseline.has(key) and current[key] == baseline[key]:
continue
if current[key] is Dictionary and baseline.get(key) is Dictionary and result.get(key) is Dictionary:
result[key] = _merge_legacy_changes(result[key], baseline[key], current[key])
else:
result[key] = current[key]
return result
func restore_snapshot(snapshot: Dictionary) -> bool:
var parsed := _parse_snapshot(snapshot)
var version: Variant = _value(snapshot, &"version", null)
if not version is int or version not in [1, SNAPSHOT_VERSION]:
last_error = "Unsupported Relationship snapshot version."
return false
var legacy: Variant = snapshot if version == 1 else _value(snapshot, &"legacy", null)
var originals: Variant = [] if version == 1 else _value(snapshot, &"original_relationships", null)
if not legacy is Dictionary or not originals is Array:
last_error = "Malformed Relationship snapshot container."
return false
if version == SNAPSHOT_VERSION and _value(snapshot, &"relationships", null) != _value(legacy, &"relationships", null):
last_error = "Legacy compatibility projection disagrees with its source."
return false
var original_records: Dictionary = {}
for raw: Variant in originals:
var state := OriginalStateType.from_snapshot(raw)
if state == null or original_records.has(state.subject_id()):
last_error = "Invalid or duplicate original relationship."
return false
if not _original_identity_validator.is_valid() or not bool(_original_identity_validator.call(state.create_snapshot())):
last_error = "Invalid original relationship identity reference."
return false
original_records[state.subject_id()] = state.create_snapshot()
var parsed := _parse_snapshot(legacy)
if not bool(parsed.get(&"ok", false)):
last_error = String(parsed.get(&"error", "Invalid Relationship snapshot."))
return false
@@ -370,12 +483,18 @@ func restore_snapshot(snapshot: Dictionary) -> bool:
_pair_index = parsed[&"pair_index"]
_threshold_candidates = parsed[&"threshold_candidates"]
_next_candidate_sequence = int(parsed[&"next_candidate_sequence"])
_original_records = original_records
_legacy_raw = legacy.duplicate(true)
_container_raw = snapshot.duplicate(true) if version == SNAPSHOT_VERSION else {}
_legacy_baselines = {}
for state: RelationshipState in get_relationships():
_legacy_baselines[state.relationship_id] = state.create_snapshot()
last_error = ""
return true
func _parse_snapshot(snapshot: Dictionary) -> Dictionary:
if int(_value(snapshot, &"version", 0)) != SNAPSHOT_VERSION:
if not _value(snapshot, &"version", null) is int or _value(snapshot, &"version", null) != 1:
return {&"ok": false, &"error": "Unsupported Relationship snapshot version."}
var raw_relationships: Variant = _value(snapshot, &"relationships", null)
var raw_candidates: Variant = _value(snapshot, &"threshold_candidates", null)
@@ -384,7 +503,7 @@ func _parse_snapshot(snapshot: Dictionary) -> Dictionary:
var relationships: Dictionary = {}
var pair_index: Dictionary = {}
for raw_state: Variant in raw_relationships:
if not raw_state is Dictionary:
if not raw_state is Dictionary or not _legacy_types_valid(raw_state):
return {&"ok": false, &"error": "Relationship snapshot entry is malformed."}
var state := RelationshipStateType.from_snapshot(raw_state)
if not state.is_valid() or relationships.has(state.relationship_id):
@@ -398,6 +517,17 @@ func _parse_snapshot(snapshot: Dictionary) -> Dictionary:
for raw_candidate: Variant in raw_candidates:
if not raw_candidate is Dictionary:
return {&"ok": false, &"error": "Threshold candidate is malformed."}
for key: String in ["candidate_id", "relationship_id", "metric_id"]:
if not (raw_candidate.get(key) is String or raw_candidate.get(key) is StringName):
return {&"ok": false, &"error": "Threshold candidate identity type is invalid."}
if not raw_candidate.get("deduplication_key") is String or not raw_candidate.get("threshold") is int or not raw_candidate.get("created_at") is int or not raw_candidate.get("requires_unlock_event") is bool:
return {&"ok": false, &"error": "Threshold candidate field type is invalid."}
var actors: Variant = raw_candidate.get("actor_ids")
if not actors is Array or actors.size() != 2:
return {&"ok": false, &"error": "Threshold candidate actor collection is invalid."}
for actor: Variant in actors:
if not (actor is String or actor is StringName):
return {&"ok": false, &"error": "Threshold candidate actor type is invalid."}
var candidate: Dictionary = raw_candidate.duplicate(true)
var candidate_id := StringName(candidate.get(&"candidate_id", candidate.get("candidate_id", "")))
var relationship_id := StringName(candidate.get(&"relationship_id", candidate.get("relationship_id", "")))
@@ -405,6 +535,8 @@ func _parse_snapshot(snapshot: Dictionary) -> Dictionary:
return {&"ok": false, &"error": "Threshold candidate reference is invalid."}
candidate_ids[candidate_id] = true
candidates.append(candidate)
if not _value(snapshot, &"next_candidate_sequence", null) is int:
return {&"ok": false, &"error": "Relationship candidate sequence type is invalid."}
var next_sequence := int(_value(snapshot, &"next_candidate_sequence", 0))
if next_sequence < 1:
return {&"ok": false, &"error": "Relationship candidate sequence is invalid."}
@@ -417,6 +549,45 @@ func _parse_snapshot(snapshot: Dictionary) -> Dictionary:
}
static func _legacy_types_valid(raw: Dictionary) -> bool:
for key: String in ["relationship_id", "actor_a_id", "actor_b_id"]:
if not raw.get(key) is String:
return false
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:
return false
for key: String in ["metrics", "action_counts", "daily_metric_deltas", "flags", "crossed_thresholds"]:
if raw.has(key) and not raw[key] is Dictionary:
return false
for key: String in ["trust", "respect", "affection", "fear", "dependence", "affinity", "obligation"]:
var metrics: Dictionary = raw.get("metrics", raw)
if metrics.has(key) and not metrics[key] is int:
return false
for key: String in ["action_counts", "daily_metric_deltas"]:
for value: Variant in raw.get(key, {}).values():
if not value is int:
return false
for key: String in ["metrics", "action_counts", "daily_metric_deltas", "flags", "crossed_thresholds"]:
for identity: Variant in raw.get(key, {}):
if not identity is String:
return false
for value: Variant in raw.get("flags", {}).values():
if not value is bool:
return false
for value: Variant in raw.get("crossed_thresholds", {}).values():
if not value is Array:
return false
for threshold: Variant in value:
if not threshold is int:
return false
if raw.has("route_id") and not raw["route_id"] is String:
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
func _register_threshold_candidate(
state: RelationshipState,
metric_id: StringName,
@@ -0,0 +1,153 @@
extends SceneTree
const Model = preload("res://scripts/relationship/original_relationship_state.gd")
const Service = preload("res://scripts/relationship/relationship_service.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _expect(value: bool, message: String) -> void:
_checks += 1
if not value:
_failures += 1
push_error(message)
static func fixture() -> Dictionary:
# Validation fixtures, never approved initialization or range limits.
return {"subject_id": "heroine_anri", "protagonist_id": "player", "rival_id": "rival_rai",
"affection": 142.5, "lust": 23.25, "netorare": 11, "shield_current": 90.5, "shield_max": 142.5}
func _service() -> RelationshipService:
return Service.new().configure_original_identity_validator(
func(p: Dictionary) -> bool:
return p["subject_id"] == "heroine_anri" and p["protagonist_id"] == "player" and p["rival_id"] == "rival_rai"
)
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:
_alias_mutations()
var service := _service()
var before := var_to_bytes(service.create_snapshot())
var unset := service.get_original_relationship(&"heroine_anri")
_expect(not unset[&"configured"] and unset[&"affection"] == null, "missing record is unset")
unset[&"affection"] = 999
_expect(var_to_bytes(service.create_snapshot()) == before, "reads own their dictionaries")
var payload := fixture()
var payload_bytes := var_to_bytes(payload)
_expect(service.register_original_relationship(payload).is_success(), "complete directional fixture registers")
payload["affection"] = 0
_expect(service.get_original_relationship(&"heroine_anri")["affection"] == 142.5, "caller cannot mutate stored facts")
_expect(not service.register_original_relationship(fixture()).is_success(), "duplicate subject rejected")
var saved := service.create_snapshot()
var restored := _service()
_expect(restored.restore_snapshot(saved), "new-only v2 restores")
_expect(var_to_bytes(restored.create_snapshot()) == var_to_bytes(saved), "new-only exact bytes")
_expect(var_to_bytes(fixture()) == payload_bytes, "fixture is explicit and stable")
for field: String in Model.FIELDS:
var missing := fixture()
missing.erase(field)
_expect(Model.from_snapshot(missing) == null, "missing required field " + field)
for value: Variant in [null, true, "12", INF, NAN, {}, []]:
var wrong := fixture()
wrong["lust"] = value
_expect(Model.from_snapshot(wrong) == null, "wrong/nonfinite numeric value rejected")
for patch: Dictionary in [{"shield_max": 1}, {"shield_current": -1}, {"shield_current": 143}, {"rival_id": "player"}, {"subject_id": "player"}]:
var wrong := fixture()
wrong.merge(patch, true)
_expect(Model.from_snapshot(wrong) == null, "value/identity invariant rejected")
var zero := fixture()
for field: String in ["affection", "lust", "netorare", "shield_current", "shield_max"]:
zero[field] = 0
_expect(Model.from_snapshot(zero) != null, "explicit zero is configured")
var legacy := {"version": 1, "relationships": [{"relationship_id": "old", "actor_a_id": "player", "actor_b_id": "heroine_anri",
"metrics": {"affinity": 117, "dependence": 29, "extension": {"opaque": [3, "keep"]}}, "extra": ["raw", 2.5]}],
"threshold_candidates": [], "next_candidate_sequence": 1, "extension": {"version": 70}}
var legacy_bytes := var_to_bytes(legacy)
var migrated := _service()
_expect(migrated.restore_snapshot(legacy), "legacy aliases still usable")
_expect(migrated.get_relationship(&"old").get_metric(&"affection") == 100, "legacy normalization behavior retained")
_expect(not migrated.get_original_relationship(&"heroine_anri")[&"configured"], "legacy never configures original facts")
_expect(var_to_bytes(migrated.create_snapshot()[&"legacy"]) == legacy_bytes, "all raw legacy values and extensions preserved")
_expect(var_to_bytes(legacy) == legacy_bytes, "legacy input bytes untouched")
_expect(migrated.register_original_relationship(fixture()).is_success(), "mixed state registers independently")
var mixed := migrated.create_snapshot()
_expect(restored.restore_snapshot(mixed), "mixed snapshot restores")
_expect(var_to_bytes(restored.create_snapshot()) == var_to_bytes(mixed), "mixed exact bytes")
_expect(migrated.apply_value_change(&"old", &"trust", 3, 0).is_success(), "legacy changes still apply")
var changed: Dictionary = migrated.create_snapshot()[&"legacy"]
_expect(changed["relationships"][0]["metrics"]["affinity"] == 117 and changed["relationships"][0]["metrics"]["trust"] == 3, "changed metric preserves raw alias")
for version: Variant in [0, 3, "2", 2.0, true]:
var bad := mixed.duplicate(true)
bad[&"version"] = version
_rejected(restored, bad)
var duplicate := mixed.duplicate(true)
duplicate[&"original_relationships"].append(fixture())
_rejected(restored, duplicate)
for patch: Dictionary in [{"rival_id": "unknown"}, {"protagonist_id": "unknown"}, {"lust": false}, {"shield_max": 10}]:
var bad := mixed.duplicate(true)
bad[&"original_relationships"][0].merge(patch, true)
_rejected(restored, bad)
var bad_legacy := legacy.duplicate(true)
bad_legacy["relationships"][0]["metrics"]["affinity"] = "117"
_rejected(restored, bad_legacy)
bad_legacy = legacy.duplicate(true)
bad_legacy["relationships"].append(bad_legacy["relationships"][0].duplicate(true))
_rejected(restored, bad_legacy)
print("Original relationship model: %d checks, %d failures" % [_checks, _failures])
quit(_failures)
func _rejected(service: RelationshipService, payload: Dictionary) -> void:
var before := var_to_bytes(service.create_snapshot())
var input := var_to_bytes(payload)
_expect(not service.restore_snapshot(payload), "corrupt snapshot rejected")
_expect(var_to_bytes(service.create_snapshot()) == before, "rejection preserves prior state")
_expect(var_to_bytes(payload) == input, "rejection preserves input bytes")
@@ -0,0 +1 @@
uid://cqxwpbkgjtnsb
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
uid://cqxwpbkgjtnsc
+14
View File
@@ -1,6 +1,20 @@
{
"schema_version": 1,
"suite_policies": {
"tests/s4/test_original_relationship_model.gd": {
"source_sha256": "c74b243dbbb54dba9bbc165e29508cca910f0beb63e32d25ebbb8ea606dccce9",
"ordinary_cleanup_prepared": false,
"ordinary_control_labels": [],
"old_bounded_teardown": false,
"new_audio_QA": false
},
"tests/s4/test_original_relationship_runtime.gd": {
"source_sha256": "c56bdb99dd43a4e7ceb3ca2861e8db39972c5c4e805126a77e2f7da32ca5bdc9",
"ordinary_cleanup_prepared": false,
"ordinary_control_labels": [],
"old_bounded_teardown": false,
"new_audio_QA": false
},
"tests/ui/test_selection_departure.gd": {
"source_sha256": "3137f36f547514f25ce6ac775fa7a5141d12f83ca4b95da0be45fecfafe9d9b4",
"ordinary_cleanup_prepared": true,
+568
View File
@@ -0,0 +1,568 @@
#!/usr/bin/env python3
"""Root-owned narrow P18 collection using the unchanged guarded phase protocol."""
import argparse
import hashlib
import json
import os
import re
import stat
from pathlib import Path
import shutil
import sys
import uuid
sys.dont_write_bytecode = True
import run_all_tests as shared
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',
]
def collect(args, root, out, refs, markers, contracts, policy, contract_path, contract_sha, parse_paths):
source_bytes = sum(pin['bytes'] for pin in refs.values())
free = shutil.disk_usage(out.parent).free
needed = source_bytes + shared.MIN_SPACE
shared.require(free >= needed, 'fresh source and phase disk admission')
outer = shared.module('p18_owned_outer', shared.SUPPORT / 'owned_outer.py')
shared.require(not outer.native.helper['live_engines'](), 'another Godot owns CPU')
engine = shutil.which('godot')
shared.require(engine and Path(engine).is_file(), 'native launcher exists')
engine = str(Path(engine).resolve())
out.mkdir(parents=True, exist_ok=False)
stage = out / 'stage'
stage.mkdir()
(out / 'starters').mkdir()
(out / 'bootstrap').mkdir()
for name, pin in shared.read(shared.SUPPORT / 'bootstrap-scripts.json').items():
data = pin['text'].encode('utf-8')
shared.require(hashlib.sha256(data).hexdigest() == pin['sha256'], 'unchanged external bootstrap bytes')
(out / 'bootstrap' / name).write_bytes(data)
protected = shared.default_player(root)
expected = {name: {'bytes': pin['bytes'], 'sha256': pin['sha256']} for name, pin in refs.items()}
selected = {'root_before': refs, 'map': {name: pin['sha256'] for name, pin in refs.items()}, 'timeout': args.timeout}
for name, pin in refs.items():
target = stage / name
target.parent.mkdir(parents=True, exist_ok=True)
shared.copy_source(Path(pin['path']), target)
shared.require(shared.inventory(stage, stage=True) == expected, 'complete private source copy')
public = (stage / 'project.godot').read_bytes()
shared.write(out / 'parse-paths.json', parse_paths)
run_id = uuid.uuid4().hex
shared.write(out / 'runner-input.json', dict(protocol='guarded-p18-v2', run_id=run_id,
owner_runner_pid=os.getpid(), project_root=str(root), stage=str(stage), roster=ROSTER,
selected_source_map=selected['map'], marker_contract_sha256=contract_sha,
runner_sha256=shared.sha(Path(__file__)), shared_runner_sha256=shared.sha(shared.HERE / 'run_all_tests.py'),
marker_module_sha256=shared.sha(shared.HERE / 'strict_test_markers.py'),
engine=engine, engine_sha256=shared.sha(engine), default_player_before=protected,
formal_acceptance=False, original_unit_complete=False))
shared.write(out / 'source-start.json', refs)
diagnostics = shared.module('p18_diagnostics', shared.SUPPORT / 'diagnostic_gate.py')
owners = shared.module('p18_owners', shared.SUPPORT / 'ordinary_proof_gate.py')
audio = shared.module('p18_audio', shared.SUPPORT / 'audio_evidence_gate.py')
phases, rows, previous = [], [], {}
error = None
try:
for phase, test in [('import', None), ('parse', None)] + [(str(i).zfill(3), name) for i, name in enumerate(ROSTER)]:
shared.require(shared.sha(contract_path) == contract_sha, 'marker contract unchanged')
row = shared.execute_phase(engine, stage, out, phase, test, selected, expected, previous,
policy, contracts, parse_paths, public, protected, root, outer, markers, diagnostics, owners, audio)
archive_profiles(out, row)
phases.append(row)
if test:
row.update(sequence=len(rows), godot=engine)
rows.append(row)
(out / 'all-tests.json').write_text(json.dumps(rows, ensure_ascii=False, indent=2) + '\n')
print(('PASS ' if row['passed'] else 'FAIL ') + test, flush=True)
shared.require(row['passed'], 'first failed phase stops without retry: ' + phase)
except BaseException as caught:
error = type(caught).__name__ + ': ' + str(caught)
finally:
cleanup, cleanup_errors = outer.finish_captured(outer.OWNED)
guard_errors = []
try:
shared.require(shared.source_refs(root) == refs, 'whole source unchanged')
shared.require(shared.sha(contract_path) == contract_sha, 'marker contract unchanged at end')
shared.require(shared.default_player(root) == protected, 'default profile untouched')
shared.require(shared.inventory(stage, stage=True) == dict(expected, **previous), 'whole stage exact')
except BaseException as caught:
guard_errors.append(type(caught).__name__ + ': ' + str(caught))
ended = bool(outer.OWNED) and all(row['engine_ended'] is True for row in cleanup)
passed = error is None and not cleanup_errors and not guard_errors and ended and len(phases) == len(ROSTER) + 2 and all(row['passed'] for row in phases)
context = shared.totals(rows, ROSTER)
shared.write(out / 'runner-ended.json', dict(context, protocol='guarded-p18-v2', run_id=run_id,
owner_runner_pid=os.getpid(), passed=passed, engine_ended=ended,
actual_captured_PIDs=[process.pid for process in outer.OWNED], final_owned_cleanup=cleanup,
cleanup_errors=cleanup_errors, guard_errors=guard_errors, error=error, phases=phases,
all_tests_sha256=shared.sha(out / 'all-tests.json') if (out / 'all-tests.json').exists() else None,
default_player_after=shared.default_player(root), formal_acceptance=False,
original_unit_complete=False, independent_review='pending', native_Windows='pending'))
if passed:
seal(out, refs, engine, contract_sha)
# Root also exercises precisely the verifier that ordinary Runner uses.
verify(out, refs, markers, contracts, policy, contract_sha, parse_paths)
print(json.dumps(dict(context, passed=passed, error=error, cleanup_errors=cleanup_errors, guard_errors=guard_errors)), flush=True)
return 0 if passed else 1
# Receipt file names are always relative; absolute paths in native receipts are
# historical identity strings only and are never opened by the verifier.
def require(ok, reason):
shared.require(ok, reason)
def digest(value):
return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(',', ':'),
ensure_ascii=False).encode()).hexdigest()
def content_map(refs):
return {name: {'bytes': pin['bytes'], 'sha256': pin['sha256']}
for name, pin in refs.items()}
def plain_path(path):
path = Path(path)
require('..' not in path.parts, 'parent traversal refused')
absolute = Path(os.path.abspath(path))
for part in [absolute, *absolute.parents]:
require(not part.is_symlink(), 'symlink path refused: ' + str(part))
return absolute
def confined(out, name):
require(type(name) is str and name and '\\' not in name, 'ordinary relative evidence name')
path = Path(name)
require(not path.is_absolute() and '..' not in path.parts
and path.as_posix() == name and name != '.', 'evidence path escape or alias')
result = plain_path(out / path)
require(result.is_relative_to(out), 'evidence confined to output')
return result
def inventory(out):
result = {}
require(out.is_dir(), 'output directory required')
for directory, dirs, files in os.walk(out, followlinks=False):
for name in dirs + files:
p = Path(directory) / name
mode = p.lstat().st_mode
require(stat.S_ISREG(mode) or stat.S_ISDIR(mode), 'nonordinary evidence refused')
if stat.S_ISREG(mode):
require(p.stat().st_nlink == 1, 'hardlinked evidence refused')
result[p.relative_to(out).as_posix()] = {
'bytes': p.stat().st_size, 'sha256': shared.sha(p)}
return result
def unique_object(pairs):
result = {}
for key, value in pairs:
require(key not in result, 'duplicate JSON key')
result[key] = value
return result
def read_json(path):
return json.loads(path.read_text(encoding='utf-8'), object_pairs_hook=unique_object,
parse_constant=lambda token: require(False, 'nonfinite JSON ' + token))
def archive_profiles(out, row):
for label in ('startup', 'runtime'):
value = row.get('whole_' + label + '_profile_after')
if value is None:
continue
dest = out / 'profiles' / row['phase'] / label
dest.mkdir(parents=True, exist_ok=False)
source = plain_path(value['path'])
require(shared.inventory(source) == value['files'], 'whole owned profile before archive')
for name in value['files']:
target = confined(out, str((dest / name).relative_to(out)))
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(source / name, target)
require(shared.inventory(dest) == value['files'] == shared.inventory(source),
'whole owned profile archive exact')
def seal(out, refs, engine, contract_sha):
# Last file written: an interrupted collection has no complete receipt.
shutil.copyfile(engine, out / 'engine.bin')
require(shared.sha(out / 'engine.bin') == shared.sha(engine), 'engine archive exact')
source = content_map(refs)
shared.write(out / 'receipt.json', {
'schema': 'guarded-p18-v2', 'source_map': source,
'parse_checks': len([name for name in refs if name.endswith('.gd')]),
'source_map_sha256': digest(source),
'project_settings_sha256': source['project.godot']['sha256'],
'marker_contract_sha256': contract_sha,
'engine_sha256': shared.sha(engine),
'files': inventory(out),
})
def cleanup_gate(value):
require(value['owned_child_created'] is True and value['engine_ended'] is True
and value['actual_process_exit_code'] == 0 and value['errors'] == [],
'successful actual owned cleanup')
attempts = value['attempts']
require(1 <= len(attempts) <= 3, 'bounded cleanup attempts')
for index, row in enumerate(attempts):
require(row['attempt'] == index + 1 and row['errors'] == []
and row['wait_timeout_seconds'] == 5 and row['wait_completed'] is True
and row['wait_returned_code'] == 0 and row['poll_after'] == 0,
'complete bounded owned wait')
def outer_cleanup(rows, pids):
require([r['pid'] for r in rows] == pids, 'exact captured cleanup PID order')
for row in rows:
require(row['engine_ended'] is True and row['outer_cleanup_errors'] == []
and row['foreign_process_or_group_signals'] == 0
and row['outer_maximum_helper_invocations'] == 3
and row['helper_maximum_waits'] == 3 and row['per_wait_timeout_seconds'] == 5,
'unchanged outer cleanup contract')
cleanup_gate(row['cleanup'])
def verify(out, refs, markers, contracts, policy, contract_sha, parse_paths):
receipt_path = confined(out, 'receipt.json')
require(receipt_path.is_file(), 'regular final receipt required')
manifest = read_json(receipt_path)
require(manifest['schema'] == 'guarded-p18-v2', 'current receipt schema required')
files = manifest['files']
require(type(files) is dict and files, 'complete raw manifest')
for name, pin in files.items():
p = confined(out, name)
require(p.is_file() and set(pin) == {'bytes', 'sha256'}
and type(pin['bytes']) is int and pin['bytes'] >= 0
and re.fullmatch('[0-9a-f]{64}', pin['sha256']) is not None,
'ordinary artifact pin')
actual = inventory(out)
receipt_pin = actual.pop('receipt.json')
require(actual == files, 'complete raw file set and SHA-256 equality')
source = content_map(refs)
require(manifest['source_map'] == source and manifest['source_map_sha256'] == digest(source),
'current whole source content map independent of checkout path')
require(manifest['project_settings_sha256'] == source['project.godot']['sha256']
and manifest['marker_contract_sha256'] == contract_sha, 'current project/marker pins')
def read(name):
require(name in files, 'required raw file missing: ' + name)
return read_json(confined(out, name))
def text(name):
require(name in files, 'required raw transport missing: ' + name)
return confined(out, name).read_text(encoding='utf-8')
start, end = read('runner-input.json'), read('runner-ended.json')
require(start['protocol'] == end['protocol'] == 'guarded-p18-v2'
and start['run_id'] == end['run_id']
and re.fullmatch('[0-9a-f]{32}', start['run_id']) is not None,
'same current run')
require(type(start['owner_runner_pid']) is int and start['owner_runner_pid'] > 0
and start['owner_runner_pid'] == end['owner_runner_pid'], 'same collector owner')
require(start['roster'] == ROSTER and len(set(ROSTER)) == 7, 'exact seven unique tests')
require(start['selected_source_map'] == {n: p['sha256'] for n, p in source.items()}
and content_map(read('source-start.json')) == source, 'whole selected source pins')
for field, name in [('runner_sha256', 'tools/run_p18_relationship_tests.py'),
('shared_runner_sha256', 'tools/run_all_tests.py'),
('marker_module_sha256', 'tools/strict_test_markers.py')]:
require(start[field] == source[name]['sha256'], 'current collector/shared/marker pin')
require(start['marker_contract_sha256'] == contract_sha, 'marker contract pin')
# Resolve only the locally configured launcher, never the receipt's engine path.
engine = shutil.which('godot')
require(engine and shared.sha(engine) == start['engine_sha256']
== manifest['engine_sha256'] == files['engine.bin']['sha256'],
'current native engine and retained bytes pinned')
derived_paths = ['res://' + name for name in sorted(refs) if name.endswith('.gd')]
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
and end['cleanup_errors'] == [] and end['guard_errors'] == [],
'finished successful collection without guard/cleanup error')
for receipt in (start, end):
require(receipt['formal_acceptance'] is False and receipt['original_unit_complete'] is False,
'backend evidence never completes original unit')
before = start['default_player_before']
require(before == end['default_player_after'] and before['logs_and_locks_included'] is True
and type(before['existed']) is bool and type(before['files']) is dict,
'complete default player before/after equality')
for name, pin in before['files'].items():
confined(out, name) # Validate the inventory name; never open the player.
require(set(pin) == {'bytes', 'sha256'} and type(pin['bytes']) is int
and pin['bytes'] >= 0 and re.fullmatch('[0-9a-f]{64}', pin['sha256']),
'whole default player file pin')
require(before['existed'] or before['files'] == {}, 'absent player has no files')
native = shared.module('p18_verify_native', shared.SUPPORT / 'owned_diagnostic_process.py')
diagnostics = shared.module('p18_verify_diagnostics', shared.SUPPORT / 'diagnostic_gate.py')
owners = shared.module('p18_verify_owners', shared.SUPPORT / 'ordinary_proof_gate.py')
# Helpers are loaded for their pure gates only. No run/probe/cleanup is called.
origin = Path(start['stage']).parent
require(Path(start['stage']) == origin / 'stage' and origin.is_absolute()
and '..' not in origin.parts, 'original output identity')
def relocated(value):
if isinstance(value, dict):
return {relocated(k): relocated(v) for k, v in value.items()}
if isinstance(value, list):
return [relocated(v) for v in value]
if isinstance(value, str) and value.startswith(str(origin) + '/'):
return str(confined(out, value[len(str(origin)) + 1:]))
return value
def proof_read(path):
path = plain_path(path)
require(path.is_relative_to(out), 'proof path stays in output')
return relocated(read(path.relative_to(out).as_posix()))
owners.read = proof_read
phases = end['phases']
require(len(phases) == 9, 'exact nine phases')
tests = read('all-tests.json')
require(len(tests) == 7 and end['all_tests_sha256'] == files['all-tests.json']['sha256'],
'complete test rows')
pids = []
for index, (phase, test) in enumerate([('import', None), ('parse', None)]
+ [(str(i).zfill(3), n) for i, n in enumerate(ROSTER)]):
row = read(phase + '-receipt.json')
expected_row = dict(row)
if test:
expected_row.update(sequence=index - 2, godot=start['engine'])
require(tests[index - 2] == expected_row, 'test row matches raw phase receipt')
require(phases[index] == expected_row and row['phase'] == phase and row['test'] == test
and row['passed'] is True and row['failed'] is False
and row['validation_error'] is None and not row.get('guard_error')
and row['default_player_entire_profile_unchanged'] is True,
'exact successful phase')
pid = row['actual_pid']
require(type(pid) is int and pid > 0 and pid not in pids, 'nine distinct actual PIDs')
pids.append(pid)
process = read(phase + '/process.json')
require(process == row['native_process'] and process['pid'] == pid
and process['owned_child_process_helper_sha256'] == native.HELPER_SHA
== source['tools/regression/p09_owned_engine_process.py']['sha256'],
'actual phase/process/helper agreement')
stdout, stderr, logger = (text(phase + '/' + n) for n in
('stdout.log', 'stderr.log', 'engine-native.log'))
raw = stdout + stderr
require(text(row['log']) == raw and row['log_sha256'] == files[row['log']]['sha256'],
'literal complete raw stream')
require(process['stdout_sha256'] == files[phase + '/stdout.log']['sha256']
and process['stderr_sha256'] == files[phase + '/stderr.log']['sha256'],
'process transport SHA')
require(row['raw_native_log'] == dict(files[phase + '/engine-native.log'],
path=str(origin / phase / 'engine-native.log')), 'native logger pin')
review = diagnostics.review_streams(native, {
'stdout': stdout, 'stderr': stderr, 'native_file': logger}, test)
require(row['finite_original_diagnostic_review'] == review
and diagnostics.diagnostic_result_gate(process, review)
and shared.process_gate(process, review['allowed_e31_error_lines']),
'strict diagnostics/process gates')
require(native.native_diagnostics(stdout, stderr, row['command'])
== process['strict_native_diagnostics'], 'raw banner/diagnostics recomputed')
command = [start['engine'], '--headless', '--path', start['stage']]
if phase == 'import':
command += ['--editor', '--import', '--quit', '--verbose']
else:
script = ('bootstrap/parse_full_dependencies.gd' if phase == 'parse'
else 'starters/' + phase + '.gd')
command += ['--script', str(origin / script), '--verbose']
require(row['starter_sha256'] == files[script]['sha256'], 'exact starter pin')
if test:
expected_starter = ('extends ' + json.dumps('res://' + test) + '\nconst Guard := preload('
+ json.dumps(str(origin / 'bootstrap/profile_guard.gd'))
+ ')\nfunc _initialize() -> void:\n\tif not Guard.activate():\n\t\tquit(97)\n\t\treturn\n\tsuper._initialize()\n')
require(text(script) == expected_starter, 'unchanged guarded starter bytes')
command += ['--log-file', str(origin / phase / 'engine-native.log')]
require(row['command'] == process['argv'] == command, 'exact native argv/logger')
for filename, field, event in [('started.json', 'actual_start_binding', 'ENGINE_STARTED'),
('ended.json', 'actual_end_binding', 'ENGINE_ENDED')]:
binding = read(phase + '/' + filename)
require(binding == process[field] and binding['event'] == event
and binding['actual_pid'] == binding['pid'] == pid
and binding['owner_pid'] == start['owner_runner_pid']
and binding['command'] == binding['argv'] == command, 'actual START/END ownership')
binding = process['actual_start_binding']
identity = binding['current_process_identity']
if identity is None:
require(binding['tracked_child_ended_before_initial_probe'] is True, 'early ended tracked child')
else:
require(binding['live_identity_exact'] is True and identity == {
'pid': pid, 'ppid': start['owner_runner_pid'], 'pgid': pid, 'args': ' '.join(command)},
'recorded live child PID/PPID/PGID/argv')
finish = process['actual_end_binding']
require(finish['engine_ended'] is True and finish['timed_out'] is False
and finish['lifecycle_error'] is None and finish['exit_code'] == 0
and finish['actual_process_exit_code'] == 0
and finish['start_receipt_written'] is True
and finish['monotonic_ns'] >= binding['monotonic_ns'], 'complete native END')
cleanup = read(phase + '/adapter-owned-cleanup.json')
require(cleanup == process['adapter_owned_cleanup'], 'raw adapter cleanup')
cleanup_gate(cleanup)
capture = row['outer_actual_capture']
require(read(phase + '/outer-owned-finally.json') == {
k: v for k, v in capture.items()
if k not in ('outer_receipt_written', 'outer_receipt_error', 'passed')},
'whole raw outer receipt')
require(capture['actual_captured_pids'] == [pid] and capture['passed'] is True
and capture['all_captured_ended'] is True and capture['adapter_exception'] is None
and capture['outer_cleanup_errors'] == [] and capture['outer_receipt_error'] is None
and capture['outer_receipt_written'] is True
and capture['all_cleanup_before_outer_receipt'] is True
and capture['foreign_process_or_group_signals'] == 0
and row['engine_ended'] is True and row['exit_code'] == 0, 'owned outer capture')
outer_cleanup(capture['cleanup'], [pid])
startup = read(phase + '-startup-profile-before.json')
require(startup == {'path': row['whole_startup_profile_after']['path'], 'existed': False,
'files': {}, 'before_native_spawn': True}, 'fresh startup profile')
require(row['whole_startup_profile_after']['fresh_before_engine'] is True,
'startup fresh before engine')
name = row['profile_startup_name']
require(re.fullmatch('TheTree-PCK-tests-[0-9a-f]{32}-standard-startup-' + phase, name)
and Path(startup['path']).name == name and row['private_project_fields'] == 2
and row['private_project_sha256'] == hashlib.sha256(shared.private_project(
(shared.HERE.parent / 'project.godot').read_bytes(), name)).hexdigest(),
'exact two-field startup isolation')
for label in ('startup', 'runtime'):
if label == 'runtime' and phase == 'import':
continue
whole = row['whole_' + label + '_profile_after']
archive = confined(out, 'profiles/' + phase + '/' + label)
require(archive.is_dir() and shared.inventory(archive)
== whole['files'], 'complete retained private profile')
if phase != 'import':
guard = read(phase + '-profile.json')
runtime = Path(row['whole_runtime_profile_after']['path'])
name = guard['custom_user_dir_name']
require(re.fullmatch('TheTree-PCK-tests-[0-9a-f]{32}-standard-runtime-' + phase, name)
and runtime.name == name and guard['verified'] is True
and guard['conditions'] == [True] * 5 and guard['conditions_checked'] == 5
and guard['fresh_before_parent_initialize'] is True
and guard['profile_writes_before_guard'] == 0
and guard['actual_user_data_dir'] == guard['expected_user_data_dir'] == str(runtime)
and guard['globalized_probe'] == str(runtime / 'guard-probe')
and guard['project_settings_persisted'] is False, 'all unchanged Guard conditions')
require(row['actual_private_profile'] == {
'report': guard, 'report_sha256': files[phase + '-profile.json']['sha256'],
'whole_files': row['whole_runtime_profile_after']['files']}, 'whole raw Guard report')
require(row['controlled_environment'] == {
'THE_TREE_PACK_PROFILE_NAME': name, 'THE_TREE_PACK_PROFILE_EXPECTED': str(runtime),
'THE_TREE_PACK_PROFILE_REPORT': str(origin / (phase + '-profile.json')),
'THE_TREE_FULL_PARSE_PATHS': str(origin / 'parse-paths.json'),
**({'THE_TREE_AUDIO_TEARDOWN_PROOF_DIR': str(origin / (phase + '-teardown/owned-audio')),
'THE_TREE_AUDIO_FIXTURE_CONTROL_PROOF': str(origin / (phase + '-teardown/control.json'))}
if test and policy['suite_policies'].get(test, {}).get('ordinary_cleanup_prepared') else {})},
'exact controlled Guard/proof environment')
else:
require(row['controlled_environment'] == {}, 'fresh import environment')
if phase == 'parse':
require(len(re.findall(r'(?m)^FULL_DEPENDENCY_PARSE\b', raw)) == 1
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:
parsed = markers.parse_raw(test, raw, contracts, source[test]['sha256'])
require(parsed['passed'] and row['completion'] == parsed
and row['reported_checks'] == parsed['checks']
and row['reported_failures'] == parsed['failures']
and row['completion_marker'] == parsed['marker'] and row['collector_errors'] == [],
'source-pinned raw completion marker')
require(row['source_sha256'] == row['source_sha256_end'] == source[test]['sha256'],
'exact test source before/after')
p = shared.phase_policy(test, Path(refs[test]['path']), policy['suite_policies'])
require(row['policy'] == p, 'current exact suite policy')
if p['ordinary_cleanup_prepared']:
proof = owners.ordinary_gate(confined(out, phase + '-teardown'), pid, out,
p['ordinary_control_labels'])
require(proof == relocated(row['ordinary_owner_pairs']), 'full ordinary/audio ownership proof')
require(row['generated_reports'] == {}, 'fixed narrow roster has no generated source reports')
require(end['actual_captured_PIDs'] == pids, 'all nine captured engines')
outer_cleanup(end['final_owned_cleanup'], pids)
require(all(end[k] == v for k, v in shared.totals(tests, ROSTER).items()), 'recomputed exact totals')
require(shared.inventory(confined(out, 'stage'), stage=True) == source, 'entire retained source/settings exact')
for name, pin in shared.read(shared.SUPPORT / 'bootstrap-scripts.json').items():
require(text('bootstrap/' + name).encode() == pin['text'].encode()
and files['bootstrap/' + name]['sha256'] == pin['sha256'], 'unchanged bootstrap/Guard pins')
require(inventory(out) == dict(files, **{'receipt.json': receipt_pin}),
'whole raw evidence unchanged during verification')
require(content_map(shared.source_refs(shared.HERE.parent)) == source,
'whole current source unchanged during verification')
print(json.dumps({'verified': True, 'actual_suites': 7, 'parse_checks': len(parse_paths),
'actual_checks': end['actual_all_roster_checks'], 'engines_started': 0,
'formal_acceptance': False, 'original_unit_complete': False}))
return 0
def main(argv=None):
argv = list(sys.argv[1:] if argv is None else argv)
# No argparse abbreviation, repeated flags, = aliases or positional tokens.
parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
parser.add_argument('--output', required=True)
modes = parser.add_mutually_exclusive_group()
modes.add_argument('--collect', action='store_true', help='Root only; fresh native collection')
modes.add_argument('--prepare-only', action='store_true')
seen = set()
i = 0
while i < len(argv):
flag = argv[i]
require(flag in ('--output', '--collect', '--prepare-only', '--help')
and flag not in seen, 'unknown, duplicate or malformed flag')
seen.add(flag)
if flag == '--output':
i += 1
require(i < len(argv) and argv[i] and not argv[i].startswith('--'), 'output value required')
i += 1
args = parser.parse_args(argv)
args.timeout = 1200
root = shared.HERE.parent
out = plain_path(args.output)
refs = shared.source_refs(root)
require(set(ROSTER).issubset(shared.roster(root)), 'normal discovery includes narrow roster')
markers = shared.module('p18_exact_markers', shared.HERE / 'strict_test_markers.py')
contract_path = root / 'docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json'
contract_sha = shared.sha(contract_path)
contracts = markers.load_contracts(contract_path)
require(all(name in contracts for name in ROSTER), 'complete marker roster')
policy = shared.read(shared.SUPPORT / 'suite-policy.json')
shared.policy_coverage(ROSTER, refs, policy)
for test in ROSTER:
shared.phase_policy(test, Path(refs[test]['path']), policy['suite_policies'])
for test in ROSTER[:2]:
require(contracts[test]['source_sha256_at_schema_audit'] == refs[test]['sha256'],
'new strict source-hashed marker')
parse_paths = ['res://' + name for name in sorted(refs) if name.endswith('.gd')]
if args.prepare_only:
ancestor = out.parent
while not ancestor.exists():
ancestor = ancestor.parent
free = shutil.disk_usage(ancestor).free
size = sum(p['bytes'] for p in refs.values())
print(json.dumps({'roster': ROSTER, 'source_files': len(refs), 'source_bytes': size,
'free_bytes': free, 'conservative_required_bytes': size + shared.MIN_SPACE,
'capacity_available': free >= size + shared.MIN_SPACE, 'parse_paths': len(parse_paths),
'future_native_phases': 9, 'actual_checks': None, 'engines': 0, 'stages': 0,
'formal_acceptance': False}))
return 0
if args.collect:
require(not out.exists() and out.parent.is_dir(), 'fresh output with existing parent required')
require(not out.is_relative_to(root), 'private collection outside source tree')
return collect(args, root, out, refs, markers, contracts, policy,
contract_path, contract_sha, parse_paths)
return verify(out, refs, markers, contracts, policy, contract_sha, parse_paths)
if __name__ == '__main__':
try:
raise SystemExit(main())
except (AssertionError, OSError, ValueError, KeyError, TypeError) as error:
print(type(error).__name__ + ': ' + str(error), file=sys.stderr)
raise SystemExit(1)