Merge commit '265ce30f81b1302ca374330f7129f02d6fde4697' into HEAD
This commit is contained in:
@@ -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.
|
||||
+94
@@ -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}
|
||||
+158
@@ -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
|
||||
}
|
||||
+11
@@ -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}
|
||||
+80
@@ -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
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user