P28-02-DIAG: 意味を保つ軽量化:実測上位の特定(受動的な診断)

This commit is contained in:
Agent Control Runner
2026-10-21 06:36:04 +09:00
parent ecbbb6bb16
commit d6ce6ddb95
5 changed files with 62 additions and 6 deletions
@@ -81,3 +81,11 @@ bundle 作成後にファイルを追加・編集すると inventory 不一致
原本加算は0、正式完了23/88、4/30、予算 false 6件を保持します。P27 完了は主張しません。
repair3: 最終の局所 unit を再実行して PASS(19件、0.780秒)。
## repair4: native parse の静的依存修正
- P1: probe の manifest version は、実際の Runtime の `get_script().get_script_constant_map()` から準備段階で取得します。script・key の存在と int 型を確認し、取得した値そのものを manifest 比較の期待値へ渡します。取得不能なら理由を errors に記録して終了します。値の固定や検査対象 manifest の流用はありません。
- P2: `autoload/*.gd` の class_name 宣言を確認し、probe の該当する静的参照は除去した1箇所だけでした。残る GameStateScript の preload 先、MvpContentCatalog と GuestDefinition には autoload 識別子への依存がありません。engine による compile 成功は未確認です。
- P3: checker は autoload の class_name を製品ファイルから機械的に抽出し、probe の識別子との一致を拒否します。公開読取の get_script / get_script_constant_map を既存の許可リストに追加しました。修正後 probe の通過、全抽出名の定数参照・型注釈の拒否、新しい宣言の自動検出を最小の追加 unit で確認しました。
- `python3 test_diag.py`: 20件 PASS、0.861秒。標準出力・標準エラーの生出力を UNIT_OUTPUT.txt に保存しました。変更は diag_probe.gd、check_diag.py、test_diag.py、UNIT_OUTPUT.txt、IMPLEMENTATION.md のみ。BLOCKED.md は解消済みの履歴として維持します。
- native import・parse・3 PID、正式 validation、独立レビューは提出後の Root 側で実施します。worker は engine・Runner 操作、git 書込み、製品変更、全回帰、他エージェント起動を行っていません。原本加算0、23/88、4/30、予算 false 6件を維持します。
@@ -1,3 +1,4 @@
test_autoload_class_references_rejected (__main__.DiagnosticTests) ... ok
test_bundle_missing_and_tampered (__main__.DiagnosticTests) ... ok
test_connections_and_preparation (__main__.DiagnosticTests) ... ok
test_cross_pid_only_named_kernel_fields (__main__.DiagnosticTests) ... ok
@@ -8,7 +9,7 @@ test_missing_not_zero (__main__.DiagnosticTests) ... ok
test_nonadditive_scope (__main__.DiagnosticTests) ... ok
test_pid_local_catalog_identity (__main__.DiagnosticTests) ... ok
test_queries_and_observer_rejection (__main__.DiagnosticTests) ... ok
test_real_owned_python_child_and_raw_binding (__main__.DiagnosticTests) ... RUNTIME_SCALE_PROCESS_START {"name": "import", "pid": 62682}
test_real_owned_python_child_and_raw_binding (__main__.DiagnosticTests) ... RUNTIME_SCALE_PROCESS_START {"name": "import", "pid": 70915}
ok
test_scratch_ownership (__main__.DiagnosticTests) ... ok
test_service_snapshot_inventory_matches_current_product (__main__.DiagnosticTests) ... ok
@@ -20,6 +21,6 @@ test_tick_id_and_minute_rejection (__main__.DiagnosticTests) ... ok
test_valid_raw_intervals (__main__.DiagnosticTests) ... ok
----------------------------------------------------------------------
Ran 19 tests in 0.780s
Ran 20 tests in 0.861s
OK
@@ -120,10 +120,20 @@ def check_modules(modules=None, root=ROOT):
"loaded project module outside closure")
def check_probe(text):
def autoload_class_names(root=ROOT):
paths = sorted((root / 'autoload').glob('*.gd'))
require(paths, 'autoload scripts missing')
return {name for path in paths for name in
re.findall(r'^\s*class_name\s+([A-Za-z_][A-Za-z_0-9]*)\b', path.read_text(), re.M)}
def check_probe(text, root=ROOT):
"""Conservative finite call policy, plus mutation-site checks; native parse is separate."""
code = '\n'.join(line.split('#', 1)[0] for line in text.splitlines())
tokens = re.sub(r'"(?:\\.|[^"\\])*"', '""', code)
identifiers = set(re.findall(r'\b[A-Za-z_][A-Za-z_0-9]*\b', tokens))
require(not identifiers.intersection(autoload_class_names(root)),
'autoload class_name reference in probe')
forbidden = (r'\b(?:set_script|set_process|set_physics_process|set_deferred|callv?|call_thread_safe|'
r'call_deferred_thread_group|emit_signal|subscribe|unsubscribe|initialize|reset|'
r'clear_performance_reports|register_tick_handler|unregister_tick_handler|'
@@ -153,7 +163,7 @@ def check_probe(text):
require('set_paused(false)' not in code, 'resume forbidden')
for receiver, allowed in {
'clock': {'get', 'set_paused', 'advance_minutes'},
'runtime': {'get', 'create_snapshot', 'restore_snapshot', 'get_save_policy'},
'runtime': {'get', 'get_script', 'create_snapshot', 'restore_snapshot', 'get_save_policy'},
'main': {'get'}, 'target': {'create_snapshot'},
'SaveService': {'save_snapshot', 'load_snapshot'},
'SimulationSnapshot': {'validate', 'restore'},
@@ -185,7 +195,8 @@ def check_probe(text):
'get_ticks_usec', 'get_ticks_msec', 'advance_minutes', 'save_snapshot', 'validate', 'restore',
'load_snapshot', 'free', 'merge', 'get_node', 'get_environment', 'get_process_id', 'parse_string',
'set_paused', 'get_sha256', 'open', 'get_var', 'close', 'restore_snapshot', 'connect',
'instantiate', 'add_child', 'get_save_policy', 'snapshot', 'disconnect', 'emit'
'instantiate', 'add_child', 'get_save_policy', 'snapshot', 'disconnect', 'emit',
'get_script', 'get_script_constant_map'
}, 'unapproved member call')
for path in re.findall(r'"(user://[^"\n]*)"', code):
require(path.startswith('user://p28_diag/scratch/'), 'nonprivate scratch path')
@@ -12,6 +12,7 @@ const K_FIELDS := ["absolute_minute", "speed", "paused", "snapshot_version", "_s
"_pause_request_serial", "_abort_current_tick_requested", "_last_tick_advanced"]
var clock: Variant
var runtime: Variant
var manifest_version: Variant
var main: Variant
var report: Dictionary = {}
var ticks: Array = []
@@ -151,7 +152,7 @@ func comparison(before: Dictionary, after: Dictionary) -> Dictionary:
var guest_ids: Array[String] = []
for definition: GuestDefinition in catalog.build_all_guest_definitions():
guest_ids.append(String(definition.guest_type_id))
var expected := {&"manifest_version": SimulationRuntimeService.MVP_CONTENT_MANIFEST_VERSION,
var expected := {&"manifest_version": manifest_version,
&"schema_version": MvpContentCatalog.SCHEMA_VERSION,
&"guest_class_ids": string_names(catalog.get_guest_class_ids()),
&"species_ids": string_names(catalog.get_species_ids()),
@@ -323,6 +324,21 @@ func begin() -> void:
var k0 := kernel_state()
clock.set_paused(true)
report.preparation.append({"operation": "initial_pause", "K_before": k0, "K_after": kernel_state()})
var runtime_script: Variant = runtime.get_script()
if runtime_script == null:
failed.append("Runtime script unavailable for manifest version")
finish()
return
var constants: Dictionary = runtime_script.get_script_constant_map()
if not constants.has("MVP_CONTENT_MANIFEST_VERSION"):
failed.append("Runtime constant MVP_CONTENT_MANIFEST_VERSION missing")
finish()
return
if typeof(constants["MVP_CONTENT_MANIFEST_VERSION"]) != TYPE_INT:
failed.append("Runtime constant MVP_CONTENT_MANIFEST_VERSION must be int")
finish()
return
manifest_version = constants["MVP_CONTENT_MANIFEST_VERSION"]
var path := OS.get_environment("THE_TREE_P28_FIXTURE")
report["input_sha256"] = FileAccess.get_sha256(path)
if report.input_sha256 != OS.get_environment("THE_TREE_P28_FIXTURE_SHA"):
@@ -226,6 +226,26 @@ class DiagnosticTests(unittest.TestCase):
with self.subTest(mutation=mutation[-100:]), self.assertRaises(ValueError):
check.check_probe(mutation)
def test_autoload_class_references_rejected(self):
source = (check.HERE / 'diag_probe.gd').read_text()
check.check_probe(source)
names = check.autoload_class_names()
self.assertTrue(names)
for name in names:
for statement in (f'var dependency = {name}.MVP_CONTENT_MANIFEST_VERSION',
f'var dependency: {name}'):
with self.subTest(statement=statement), self.assertRaisesRegex(
ValueError, 'autoload class_name reference'):
check.check_probe(source + '\nfunc illegal():\n\t' + statement + '\n')
# Discover new product declarations without maintaining a name list.
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / 'autoload').mkdir()
(root / 'autoload/new_service.gd').write_text('class_name NewAutoloadService\nextends Node\n')
self.assertEqual(check.autoload_class_names(root), {'NewAutoloadService'})
with self.assertRaisesRegex(ValueError, 'autoload class_name reference'):
check.check_probe(source + '\nvar dependency: NewAutoloadService\n', root)
def test_service_snapshot_inventory_matches_current_product(self):
source = (check.ROOT / 'autoload/simulation_runtime.gd').read_text()
body = source.split('func create_snapshot() -> Dictionary:', 1)[1].split('\nfunc restore_snapshot', 1)[0]