Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1875b032b7 | ||
|
|
806de25068 | ||
|
|
e0420d57a3 |
@@ -0,0 +1,75 @@
|
|||||||
|
# P19-03 実装提出報告
|
||||||
|
|
||||||
|
提出状態。受入は未確定。原受入「7名×3ルートの到達表があり、未制作内容は欠損として残る。」を維持する。14/88は変更しない。
|
||||||
|
|
||||||
|
## 変更範囲
|
||||||
|
|
||||||
|
修復開始点e0420d57a381a1644357f0590536e1d47c9bca9fから、このディレクトリの指定5ファイルのみを変更した。原本、原子要件、work packages、decisions、TASK_SPEC、選定記録、frozen pins、ゲーム入力、marker registryは変更していない。git書込み、他worker起動、Runner操作、ゲーム起動は行っていない。素材追加なし。
|
||||||
|
|
||||||
|
## 要件と検査の対応
|
||||||
|
|
||||||
|
|TASK_SPEC implementation_requirements順|成果物|検査|
|
||||||
|
|---|---|---|
|
||||||
|
|1: 原7名×3ルート|REACHABILITY_TABLE.json / .md|expectedのPEOPLE/ROUTES、原本118–124行、atomic entityと21セルの完全一致。test_exact_cell_product|
|
||||||
|
|2: 8カテゴリ、共通/人物固有の分離|JSON common_rules / cells.categories|CATEGORIES全件のcanonical比較。test_categories_and_citations|
|
||||||
|
|3: 原本本文・行・status・anchorのsource SHA・owner|JSONの各共通項目/人物固有項目、catalog_evidence|expected.evidence/cite、verify。test_stale_record_evidence / test_owner_and_status|
|
||||||
|
|4: 欠損、理由、担当、未知値pending|各character_specific、pending、natural_route_complete=false|test_no_invented_values_or_completion。D05/D08は未回答のまま|
|
||||||
|
|5: docs内stdlibチェッカー/自己試験|check_reachability_table.py / test_reachability_table.py|stdlib importsのみ。選択対象外のdocs内配置|
|
||||||
|
|6: read-only --check、厳密な表/Markdown|verify / verify_records / markdown|test_json_and_types / test_markdown_drift。--checkは書込み分岐を通らない|
|
||||||
|
|7: 全frozen source mapと各identity|binding / selection|1368選択ファイルの完全map、marker/policy/engine、184-suite roster、329 .gd、baseline、spec→pins SHA。test_full_frozen_map_and_identities / test_real_source_and_baseline_changes|
|
||||||
|
|8: UI/Runtime/save/Windowsの適用性|JSON route_support、MDルート別適用性|各ルートをcanonical比較。汎用UIラベル、原本登録Runtime、専用snapshotの限定証拠をcommon_rulesで参照|
|
||||||
|
|9: 有意な負例と3種の正例|test_reachability_table.py|欠落/重複/誤人物/誤route/カテゴリ/引用/古い本文・SHA/値創作/誤完成/owner・status/JSON型/Markdown、実ファイル読込差替え、map差分とpins/marker/policy/engine改変。全3statusを含む正例|
|
||||||
|
|10: P13パターン・原文保全|checkerのexpected/verify/markdown、原資料のbase blob照合|tools/check_p13_content_difference.pyのcanonical生成・厳密比較・read-only Git blob証拠パターンを参照。元ファイルは無変更|
|
||||||
|
|11: 報告・残課題・外部gate|本報告、JSON accounting|正式試験・全単位独立レビュー・統合/finalは未実行と明記|
|
||||||
|
|
||||||
|
原子要件はB-7.1〜B-7.6をsource_group_idsに含む61件の完全レコードを読込対象とし、JSON authority_snapshotsに保持する。人物キーと原名はそのentityから確認し、原本§6の順で並べた。原本§6は114–135行、§7は136–161行であり、実際の見出しと本文を照合した。人物別の原本仕様がない部分を共通ルールから創作していない。中断/保存の人物固有手順は原本に定義されず、work_packagesのP19-02受入が所有する欠損として残した。
|
||||||
|
|
||||||
|
指定されたoriginal_relationship_state.gdには7名のrosterはなく、汎用subject_id等の明示入力検証のみがある。既存unique catalogのヒロインはheroine_anriのみ。監査IDには既存atomic entity.key(anri/fine/tomari/amelia/liz/mimi/satsuki)を使い、他6名の実装catalog ID、ライバル対応、初期値を創作していない。この実装欠損は表の欠損であり、原7名を削減する理由にしていない。
|
||||||
|
|
||||||
|
## Repair 1: 4指摘の対応
|
||||||
|
|
||||||
|
編集前に build/owner-handoff/p19-review-r01/ の ACTUAL_OPUS_REPAIR_DIRECTION.json、ACTUAL_WHOLE_UNIT_REVIEW.json、SPEC_HASH_REPRESENTATIONS.json を読んだ。
|
||||||
|
|
||||||
|
- MEDIUM: coassignment_attack / zero_break / love_actions / threshold_route_loss と旧UIのlegacy_evidenceを追加。固定anchorの実際の本文・行・Git blob OID・file-bytes SHA-256を保持する。原本準拠statusはmissing、旧実装分類はlegacy_not_originalに統一。P18-01は一対一変換を禁止し、旧数値・日次制限は原本として未承認と明記した。UI定数は実際には98–101行であり、89–97行の無関係なラベルを除いた。共通ルール参照により各該当セルへ結び付けた。所有者・原本欠損・natural_route_complete=falseを維持。
|
||||||
|
- LOW 独立試験: validate_legacyは候補とcanonicalの比較とは独立に、git showで固定anchorのソースを読み、引用行本文とgit rev-parseによるblob OID、現ファイルとの一致を検査する。test_independent_legacy_source_evidenceは実際のJSONと実ファイルを開き、git hash-object --stdin(書込みなし)でOIDを独立計算し、関数名とshield_broken/EXCLUSIVE_LOCKEDも確認。test_legacy_evidence_required_independentlyは対象全項目の証拠削除・空配列・無条件not implemented・古いOID/本文を拒否する。
|
||||||
|
- LOW 中断参照: 全21セルのinterruption_cancellationに原本148・153行の本文とbaseline hash、および343–355行のconfirm_route_lockをlegacy_not_originalとして追加。人物固有欠損と担当は維持。
|
||||||
|
- LOW ハッシュ表示: 263a01b943d8056727335fb76977c010298e1c229077f8da76e48683ba2e89bb は TASK_SPEC.json file-bytes SHA-256。3af3d9b931edc5449796662ad69bb6f67c80b48827a86565b1952931db68dd15 は Runner git-show stripped-UTF-8 contract spec_sha256。上記SPEC_HASH_REPRESENTATIONS.jsonのunchanged_file=trueが証拠であり、両者は同じ無変更ファイルを表す。checker定数と出力にも区別を反映。Git blob OIDは別のgit_blob_oidフィールドでありfile-bytes SHA-256とは区別する。
|
||||||
|
|
||||||
|
## 実行記録(今回のローカル確認)
|
||||||
|
|
||||||
|
以下は正式Runner実行ではない。記録済み修復指示に基づき3登録コマンドをローカル実行した。
|
||||||
|
|
||||||
|
生成 — exit 0
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 -B /Users/korizatou114/Projects/TheTree-managed-worktrees/p19-03-reachability/P19-03-TABLE-implement/docs/plans/original-completion-2026-10-02/execution/p19-03/check_reachability_table.py --project-root /Users/korizatou114/Projects/TheTree-managed-worktrees/p19-03-reachability/P19-03-TABLE-implement --write
|
||||||
|
```
|
||||||
|
|
||||||
|
p19-table — exit 0
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 -B /Users/korizatou114/Projects/TheTree-managed-worktrees/p19-03-reachability/P19-03-TABLE-implement/docs/plans/original-completion-2026-10-02/execution/p19-03/check_reachability_table.py --project-root /Users/korizatou114/Projects/TheTree-managed-worktrees/p19-03-reachability/P19-03-TABLE-implement --check
|
||||||
|
```
|
||||||
|
|
||||||
|
p19-negative — exit 0
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 -B /Users/korizatou114/Projects/TheTree-managed-worktrees/p19-03-reachability/P19-03-TABLE-implement/docs/plans/original-completion-2026-10-02/execution/p19-03/test_reachability_table.py --project-root /Users/korizatou114/Projects/TheTree-managed-worktrees/p19-03-reachability/P19-03-TABLE-implement
|
||||||
|
```
|
||||||
|
|
||||||
|
p19-source-binding — exit 0
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 -B /Users/korizatou114/Projects/TheTree-managed-worktrees/p19-03-reachability/P19-03-TABLE-implement/docs/plans/original-completion-2026-10-02/execution/p19-03/check_reachability_table.py --project-root /Users/korizatou114/Projects/TheTree-managed-worktrees/p19-03-reachability/P19-03-TABLE-implement --source-binding-only
|
||||||
|
```
|
||||||
|
|
||||||
|
--writeの実際の呼出しは同一checkout内で相対スクリプトパスと --project-root . を使用(上記は絶対パス表記)。--checkはMarkdownのバイト完全一致を確認。自己試験は12件OK。source bindingは1368ファイル一致、file_equality=true、actual_commit=e0420d57a381a1644357f0590536e1d47c9bca9f、native_evidence_reuse_accepted=false。git diff --checkはexit 0。構文は上記Python実行で確認。最初の編集用here-documentは一時ファイル作成制限でexit 1、変更は行われず、python3 -cで再実行してexit 0。native full・ゲーム起動・git書込み・Runner操作・他エージェント起動は行っていない。
|
||||||
|
|
||||||
|
## 残る親成果・ゲームの欠損
|
||||||
|
|
||||||
|
- P18-01: 原本3軸/盾の通常UI、承認済み初期値、本来の全単位受入。P18-02/03: 同配置・軽減・ブレイク・接触・閾値・確定遷移。既存旧汎用モデルの閾値を原本へ転用しない。
|
||||||
|
- P19-01/02: 原本行動と全3種決闘、勝敗・費用・盾/履歴の一度だけ反映、通常入口、停止/取消/離脱/保存再開。原本状態保持のbackendを自然ルート完成としない。
|
||||||
|
- P20-01/02/03: スキル/章・全エンド・図鑑と新規からの通常到達。P26-01/02/03: 人物別必要内容、状態接続、完成資産と制作受入。人物固有イベント/条件/履歴/立ち絵差分は欠損。
|
||||||
|
- D05、D08はpending。未知の閾値・率・初期値もpendingのまま。P04の既存D05選択、P13の4件の制作followupも未解決。
|
||||||
|
- この文書単位のnative GUI/Windows実行は適用外。global same-Windows、held gestures、全ゲーム受入はopen。固定入力一致から既存native証拠の正式再利用承認を推定していない。
|
||||||
|
- Runnerの3 validator、原本§6–7と元受入に対する実装者から独立したwhole-unit Opus review、その後の通常prepare-integration/finalが必要。どの完了数・元台帳も変更していない。
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+325
@@ -0,0 +1,325 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Read-only, stdlib P19-03 documentation validator; never accepts gameplay."""
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
OUT = 'docs/plans/original-completion-2026-10-02/execution/p19-03/'
|
||||||
|
PLAN = 'docs/plans/original-completion-2026-10-02/'
|
||||||
|
BASELINE = 'docs/traceability/original/BASELINE.txt'
|
||||||
|
ANCHOR = '1b07b87d3736e777932315a4bdde8b70cbf9cb23'
|
||||||
|
BASE = '8ca9263fee1ec4374d97c01fda82b95e1df6a977'
|
||||||
|
BASE_HASH = '75e4bdbdeb379c9269553f0c776bdb9cc96e01f2d6aa5ca650d132c63a8aeb1a'
|
||||||
|
SPEC_FILE_BYTES_SHA256 = '263a01b943d8056727335fb76977c010298e1c229077f8da76e48683ba2e89bb'
|
||||||
|
# Proof: build/owner-handoff/p19-review-r01/SPEC_HASH_REPRESENTATIONS.json
|
||||||
|
# Both hashes describe the same unchanged TASK_SPEC.json.
|
||||||
|
RUNNER_CONTRACT_SPEC_SHA256 = '3af3d9b931edc5449796662ad69bb6f67c80b48827a86565b1952931db68dd15'
|
||||||
|
PINS_HASH = '6c778226a4cc6d0b0d682500759e7d4229c926a27bd2e247b00b9c3e5f1a2fb0'
|
||||||
|
ROUTES = {'love': '純愛', 'ntr': 'NTR', 'corruption': '堕落'}
|
||||||
|
PEOPLE = [('anri', 'アンリ'), ('fine', 'フィーネ'), ('tomari', 'トマリ'),
|
||||||
|
('amelia', 'アメリア'), ('liz', 'リズ'), ('mimi', 'ミミ'), ('satsuki', 'サツキ')]
|
||||||
|
CATEGORIES = ('entry_required_events', 'state_conditions', 'duel_effects',
|
||||||
|
'route_selection_effects', 'interruption_cancellation', 'save_handling',
|
||||||
|
'history_marks', 'current_reachability')
|
||||||
|
STATE = 'scripts/relationship/original_relationship_state.gd'
|
||||||
|
SERVICE = 'scripts/relationship/relationship_service.gd'
|
||||||
|
RUNTIME = 'autoload/simulation_runtime.gd'
|
||||||
|
CATALOG = 'data/characters/mvp_unique_characters.json'
|
||||||
|
|
||||||
|
EVENT = 'scripts/events/event_service.gd'
|
||||||
|
PREVIEW = 'scripts/relationship/placement_risk_preview_service.gd'
|
||||||
|
LEGACY_REASON = 'P18-01 forbids one-to-one conversion; legacy numbers/daily limits are not approved originals.'
|
||||||
|
LEGACY_RANGES = {
|
||||||
|
'coassignment_attack': [(SERVICE,264,312),(RUNTIME,6850,6850),(EVENT,1039,1039),(PREVIEW,536,565),(PREVIEW,619,640)],
|
||||||
|
'zero_break': [(SERVICE,264,312),(RUNTIME,6850,6850),(EVENT,1039,1039)],
|
||||||
|
'love_actions': [(SERVICE,246,261)],
|
||||||
|
'threshold_route_loss': [(SERVICE,315,340),(SERVICE,343,355),(RUNTIME,342,343),(EVENT,1047,1047)],
|
||||||
|
'legacy_event_ui': [('scenes/main/main.gd',98,101)],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def validate_legacy(root, document):
|
||||||
|
rules = {r['id']: r for r in document['common_rules']}
|
||||||
|
for identity, ranges in LEGACY_RANGES.items():
|
||||||
|
rule = rules[identity]
|
||||||
|
require(rule['status'] == 'missing' and rule['legacy_classification'] == 'legacy_not_original',
|
||||||
|
'legacy/original classification')
|
||||||
|
require(rule['implementation'] == 'original conformance missing; see legacy_evidence'
|
||||||
|
and rule['legacy_reason'] == LEGACY_REASON, 'unqualified legacy absence')
|
||||||
|
entries = rule.get('legacy_evidence')
|
||||||
|
require(type(entries) is list and len(entries) == len(ranges), 'missing legacy evidence')
|
||||||
|
for entry, (path, start, end) in zip(entries, ranges):
|
||||||
|
data = blob(root, ANCHOR, path)
|
||||||
|
oid = subprocess.check_output(['git','rev-parse',ANCHOR+':'+path],cwd=root).decode().strip()
|
||||||
|
require(entry == {'path':path,'anchor':ANCHOR,'git_blob_oid':oid,
|
||||||
|
'sha256':sha(data),'lines':[start,end],
|
||||||
|
'text':'\n'.join(data.decode().splitlines()[start-1:end])}, 'legacy text/blob/range')
|
||||||
|
require(data == (root/path).read_bytes(), 'legacy current source mismatch')
|
||||||
|
|
||||||
|
|
||||||
|
def sha(data):
|
||||||
|
return hashlib.sha256(data).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def require(value, message):
|
||||||
|
if not value:
|
||||||
|
raise ValueError(message)
|
||||||
|
|
||||||
|
|
||||||
|
def decode(data):
|
||||||
|
def unique(pairs):
|
||||||
|
result = {}
|
||||||
|
for k, v in pairs:
|
||||||
|
require(k not in result, 'duplicate JSON key: ' + k)
|
||||||
|
result[k] = v
|
||||||
|
return result
|
||||||
|
return json.loads(data, object_pairs_hook=unique,
|
||||||
|
parse_constant=lambda s: (_ for _ in ()).throw(ValueError(s)))
|
||||||
|
|
||||||
|
|
||||||
|
def blob(root, commit, path):
|
||||||
|
return subprocess.check_output(['git', 'show', commit + ':' + path], cwd=root)
|
||||||
|
|
||||||
|
|
||||||
|
def selection(root):
|
||||||
|
paths = [p for folder in ('autoload', 'scripts', 'scenes', 'data', 'tests', 'assets', 'tools')
|
||||||
|
for p in (root / folder).rglob('*')
|
||||||
|
if p.is_file() and p.name != '.DS_Store' and '__pycache__' not in p.parts]
|
||||||
|
paths += [root / p for p in ('project.godot', 'export_presets.cfg', 'Makefile', 'AGENTS.md')]
|
||||||
|
require(all(not p.is_symlink() for p in paths), 'selected source aliases refused')
|
||||||
|
return {p.relative_to(root).as_posix(): {'bytes': p.stat().st_size, 'sha256': sha(p.read_bytes())}
|
||||||
|
for p in sorted(paths)}
|
||||||
|
|
||||||
|
|
||||||
|
def binding(root):
|
||||||
|
raw = lambda p: (root / p).read_bytes()
|
||||||
|
require(sha(raw(OUT + 'TASK_SPEC.json')) == SPEC_FILE_BYTES_SHA256, 'immutable spec changed')
|
||||||
|
spec = decode(raw(OUT + 'TASK_SPEC.json'))
|
||||||
|
require(sha(raw(OUT + 'FROZEN_GAME_INPUT_PINS.json')) == spec['frozen_game_input_pins_sha256'] == PINS_HASH,
|
||||||
|
'frozen pins SHA differs from immutable spec')
|
||||||
|
pins = decode(raw(OUT + 'FROZEN_GAME_INPUT_PINS.json'))
|
||||||
|
require(pins['audited_game_source_anchor_commit'] == spec['audit_source_anchor'] == ANCHOR, 'anchor identity')
|
||||||
|
actual = selection(root)
|
||||||
|
require(actual == pins['source_files'], 'full selected-source map: missing/new/changed file')
|
||||||
|
for key in ('marker_contract', 'policy', 'engine'):
|
||||||
|
require(sha(Path(pins[key + '_path']).read_bytes() if key == 'engine' else raw(pins[key + '_path']))
|
||||||
|
== pins[key + '_sha256'], key + ' identity')
|
||||||
|
require(sha(raw(BASELINE)) == pins['baseline_sha256'] == BASE_HASH, 'baseline hash')
|
||||||
|
require(len(raw(BASELINE)) == pins['baseline_bytes'], 'baseline bytes')
|
||||||
|
roster = ['tests/run_tests.gd'] + sorted(p.relative_to(root).as_posix()
|
||||||
|
for p in (root / 'tests').rglob('test*.gd') if 'export' not in p.relative_to(root).parts
|
||||||
|
and p.relative_to(root).as_posix() != 'tests/s0/test_kernel_and_save.gd')
|
||||||
|
require(roster == pins['roster'], 'roster identity')
|
||||||
|
require(sum(p.endswith('.gd') for p in actual) == pins['parse_files'], 'parse selection identity')
|
||||||
|
# Audit evidence is bound to published blobs, never a future self-containing commit.
|
||||||
|
for p in (STATE, SERVICE, RUNTIME, CATALOG):
|
||||||
|
require(raw(p) == blob(root, ANCHOR, p), 'stale audited source: ' + p)
|
||||||
|
return {'actual_commit': subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=root).decode().strip(),
|
||||||
|
'selected_files': len(actual), 'anchor': ANCHOR, 'file_equality': True,
|
||||||
|
'native_evidence_reuse_accepted': False,
|
||||||
|
'TASK_SPEC_file_bytes_sha256': SPEC_FILE_BYTES_SHA256,
|
||||||
|
'Runner_git_show_stripped_UTF8_contract_spec_sha256': RUNNER_CONTRACT_SPEC_SHA256}
|
||||||
|
|
||||||
|
|
||||||
|
def expected(root):
|
||||||
|
raw = lambda p: (root / p).read_bytes()
|
||||||
|
require(sha(raw(BASELINE)) == BASE_HASH, 'baseline changed')
|
||||||
|
for name in ('atomic_requirements.json', 'work_packages.json', 'decisions.json'):
|
||||||
|
require(raw(PLAN + name) == blob(root, BASE, PLAN + name), 'original register changed: ' + name)
|
||||||
|
require(raw(OUT + 'ACTUAL_OPUS_NEXT_ORIGINAL_UNIT_SELECTION.json') ==
|
||||||
|
blob(root, BASE, OUT + 'ACTUAL_OPUS_NEXT_ORIGINAL_UNIT_SELECTION.json'), 'selection record changed')
|
||||||
|
lines = raw(BASELINE).decode().splitlines()
|
||||||
|
def cite(start, end=None):
|
||||||
|
end = end or start
|
||||||
|
return {'path': BASELINE, 'lines': [start, end],
|
||||||
|
'text': '\n'.join(lines[start - 1:end]), 'baseline_sha256': BASE_HASH}
|
||||||
|
def evidence(path, start, end):
|
||||||
|
data = blob(root, ANCHOR, path)
|
||||||
|
require(data == raw(path), 'stale source ' + path)
|
||||||
|
return {'path': path, 'sha256': sha(data), 'anchor': ANCHOR,
|
||||||
|
'lines': [start, end], 'text': '\n'.join(data.decode().splitlines()[start - 1:end])}
|
||||||
|
def item(identity, start, end, note, owner, status='missing', source=None):
|
||||||
|
return {'id': identity, 'authority': cite(start, end), 'status': status,
|
||||||
|
'implementation': source or 'original conformance missing', 'owner': owner,
|
||||||
|
'observation': note, 'gap': None if status == 'implemented' else '欠損: ' + note}
|
||||||
|
# Only narrow storage facts are implemented; simulation/route completion never follows.
|
||||||
|
rules = [
|
||||||
|
item('state_fields', 138, 143, '原本3値と盾の明示値の検証・保持のみ。初期値と更新方針は未定。',
|
||||||
|
'P18-01', 'implemented', evidence(STATE, 4, 34)),
|
||||||
|
item('affection_max_invariant', 140, 140, '入力時の最大値一致検証のみ。通常操作の回復・更新は欠損。',
|
||||||
|
'P18-01', 'partial', evidence(STATE, 20, 30)),
|
||||||
|
item('snapshot_container', 138, 143, '専用状態と旧値の分離保存・復元。人物別ルート進行保存を意味しない。',
|
||||||
|
'P18-01', 'partial', evidence(SERVICE, 423, 479)),
|
||||||
|
item('identity_registration', 138, 138, 'Runtimeは登録済み人物・相互対応を検証。既存カタログのヒロインはアンリのみ。通常ルート入口ではない。',
|
||||||
|
'P19-01', 'partial', evidence(RUNTIME, 6791, 6812)),
|
||||||
|
item('legacy_event_ui', 154, 157, '既存UIには汎用関係イベントの確認/保留ラベルがある。原本の行動・決闘選択・自然ルート到達は欠損。',
|
||||||
|
'P19-01', 'partial', evidence('scenes/main/main.gd', 98, 101)),
|
||||||
|
]
|
||||||
|
clauses = [
|
||||||
|
('defense_reduction', 141, 141, 'P18-02'), ('post_break_metric', 142, 143, 'P18-02'),
|
||||||
|
('barrier_block', 143, 143, 'P18-02'), ('coassignment_attack', 145, 145, 'P18-02'),
|
||||||
|
('zero_break', 146, 146, 'P18-02'), ('contact_after_break', 147, 147, 'P18-02'),
|
||||||
|
('threshold_route_loss', 148, 148, 'P18-03'), ('duel_types', 150, 151, 'P19-01'),
|
||||||
|
('duel_win', 152, 152, 'P19-01'), ('duel_loss', 153, 153, 'P19-01'),
|
||||||
|
('love_actions', 155, 155, 'P19-01'), ('ntr_actions', 156, 156, 'P19-01'),
|
||||||
|
('corruption_actions', 157, 157, 'P19-01'), ('history_fields', 159, 159, 'P19-02'),
|
||||||
|
('history_cycle_parentage', 160, 160, 'P19-02'), ('portrait_marks', 161, 161, 'P26-02'),
|
||||||
|
('route_skills', 132, 132, 'P20-01'), ('departure', 135, 135, 'P19-02')]
|
||||||
|
for identity, start, end, owner in clauses:
|
||||||
|
rules.append(item(identity, start, end,
|
||||||
|
'原本の共通要求。専用原本モデルにはこの更新・表示の実装なし。旧汎用モデルの数値やルートを代用しない。', owner))
|
||||||
|
for rule in rules:
|
||||||
|
if rule['id'] in LEGACY_RANGES:
|
||||||
|
rule.update(status='missing', implementation='original conformance missing; see legacy_evidence',
|
||||||
|
legacy_classification='legacy_not_original', legacy_reason=LEGACY_REASON)
|
||||||
|
rule['gap'] = '欠損: ' + rule['observation'] + ' ' + LEGACY_REASON
|
||||||
|
rule['legacy_evidence'] = []
|
||||||
|
for path, start, end in LEGACY_RANGES[rule['id']]:
|
||||||
|
entry = evidence(path, start, end)
|
||||||
|
entry['git_blob_oid'] = subprocess.check_output(
|
||||||
|
['git','rev-parse',ANCHOR+':'+path],cwd=root).decode().strip()
|
||||||
|
rule['legacy_evidence'].append(entry)
|
||||||
|
# Read complete selected atomic records, not only their condition strings.
|
||||||
|
atoms = decode(raw(PLAN + 'atomic_requirements.json'))['requirements']
|
||||||
|
atoms = [x for x in atoms if set(x.get('source_group_ids', [])) & {'B-7.' + str(i) for i in range(1, 7)}]
|
||||||
|
owners = {x['id'] for p in decode(raw(PLAN + 'work_packages.json')) for x in p.get('remaining_deliverables', [])}
|
||||||
|
require(all(r['owner'] in owners for r in rules), 'unknown original owner')
|
||||||
|
for key, name in PEOPLE:
|
||||||
|
for route in ROUTES:
|
||||||
|
require(any(x['id'] == 'AR.heroine.' + key + '.route.' + route and
|
||||||
|
x['entity'] == {'kind': 'heroine', 'key': key, 'original_name': name} for x in atoms),
|
||||||
|
'canonical atomic character/route missing')
|
||||||
|
require(all(name in lines[117 + i] for i, (_, name) in enumerate(PEOPLE)), 'original character names')
|
||||||
|
cells = []
|
||||||
|
for i, (key, name) in enumerate(PEOPLE):
|
||||||
|
for route, route_name in ROUTES.items():
|
||||||
|
refs = {
|
||||||
|
'entry_required_events': ['identity_registration', route + '_actions'],
|
||||||
|
'state_conditions': ['state_fields', 'affection_max_invariant', 'defense_reduction',
|
||||||
|
'post_break_metric', 'barrier_block', 'coassignment_attack', 'zero_break', 'contact_after_break'],
|
||||||
|
'duel_effects': ['duel_types', 'duel_win', 'duel_loss'],
|
||||||
|
'route_selection_effects': ['threshold_route_loss', route + '_actions', 'route_skills'],
|
||||||
|
'interruption_cancellation': ['departure', 'threshold_route_loss', 'duel_loss'], 'save_handling': ['snapshot_container'],
|
||||||
|
'history_marks': ['history_fields', 'history_cycle_parentage', 'portrait_marks'],
|
||||||
|
'current_reachability': ['identity_registration', 'threshold_route_loss']}
|
||||||
|
categories = {}
|
||||||
|
for category in CATEGORIES:
|
||||||
|
owner = {'entry_required_events': 'P26-01', 'state_conditions': 'P18-02',
|
||||||
|
'duel_effects': 'P19-01', 'route_selection_effects': 'P26-02',
|
||||||
|
'interruption_cancellation': 'P19-02', 'save_handling': 'P19-02',
|
||||||
|
'history_marks': 'P19-02', 'current_reachability': 'P20-02'}[category]
|
||||||
|
gap = item(key + '_' + route + '_' + category, 138, 161,
|
||||||
|
'原本未定義/D08 pending: 人物固有のイベント列・条件・中断取消・進行保存・履歴と通常到達は未制作/未証明。共通要求から固有仕様を創作しない。', owner)
|
||||||
|
gap['character_authority'] = cite(118 + i)
|
||||||
|
categories[category] = {'common_rule_refs': refs[category], 'character_specific': gap}
|
||||||
|
if category == 'interruption_cancellation':
|
||||||
|
categories[category].update(route_termination_authorities=[cite(148),cite(153)],
|
||||||
|
legacy_classification='legacy_not_original', legacy_reason=LEGACY_REASON,
|
||||||
|
legacy_evidence=[e for r in rules if r['id']=='threshold_route_loss'
|
||||||
|
for e in r['legacy_evidence'] if e['path']==SERVICE and e['lines']==[343,355]])
|
||||||
|
|
||||||
|
cells.append({'character_id': key, 'name': name, 'route_id': route, 'route': route_name,
|
||||||
|
'natural_route_complete': False, 'categories': categories})
|
||||||
|
return {'task_id': 'P19-03-TABLE', 'schema_version': 1,
|
||||||
|
'identity_policy': '原本§6の名前とatomic entity.keyを監査IDに使用。実装IDを捏造しない。カタログにはheroine_anriのみ。OriginalRelationshipStateはsubject_idを保持し7名の列挙はしない。',
|
||||||
|
'source_anchor': ANCHOR, 'common_rules': rules, 'cells': cells,
|
||||||
|
'authority_snapshots': {p: {'sha256': sha(raw(p)), 'base_commit': BASE,
|
||||||
|
'records': decode(raw(p)) if p.endswith('decisions.json') else atoms if p.endswith('atomic_requirements.json')
|
||||||
|
else [x for x in decode(raw(p)) if x['id'] in ('P18', 'P19', 'P20', 'P26')]}
|
||||||
|
for p in (PLAN + 'atomic_requirements.json', PLAN + 'work_packages.json', PLAN + 'decisions.json')},
|
||||||
|
'catalog_evidence': evidence(CATALOG, 1, 32),
|
||||||
|
'pending': {'D05': 'pending', 'D08': 'pending', 'thresholds': None, 'rates': None, 'initial_values': None},
|
||||||
|
'route_support': {r: {'UI': '欠損: 原本行動・3種決闘・原本3軸/盾の通常操作なし。既存汎用関係UIを原本ルートとしない。',
|
||||||
|
'Runtime': 'partial: 原本明示状態の登録・identity検証のみ。共通更新と通常ルート到達は欠損。',
|
||||||
|
'save': 'partial: 専用状態/旧値コンテナ保存あり。人物固有進行・中断再開は欠損。',
|
||||||
|
'Windows': '未受入: global same-Windows acceptanceはopen。文書単位のnative GUI/Windows実行は適用外。',
|
||||||
|
'evidence_refs': ['legacy_event_ui', 'identity_registration', 'snapshot_container', 'state_fields'],
|
||||||
|
'missing_mechanisms': [x[0] for x in clauses]} for r in ROUTES.values()},
|
||||||
|
'acceptance': '7名×3ルートの到達表があり、未制作内容は欠損として残る。',
|
||||||
|
'accounting': '14/88 unchanged; formal Runner validation, independent whole-unit Opus review and normal integration/final required; no game acceptance'}
|
||||||
|
|
||||||
|
|
||||||
|
def verify(candidate, canonical, root=None):
|
||||||
|
require(type(candidate) is dict, 'document must be an object')
|
||||||
|
if root is not None:
|
||||||
|
validate_legacy(root, candidate)
|
||||||
|
# Exact canonical equality also rejects additions, type coercion, invented numbers,
|
||||||
|
# owner/status promotion and unsupported completion, including nested metadata.
|
||||||
|
require(json.dumps(candidate, ensure_ascii=False, sort_keys=True) ==
|
||||||
|
json.dumps(canonical, ensure_ascii=False, sort_keys=True), 'table differs from pinned canonical audit')
|
||||||
|
|
||||||
|
|
||||||
|
def verify_records(candidate, canonical, markdown_bytes):
|
||||||
|
verify(candidate, canonical)
|
||||||
|
require(markdown_bytes == markdown(canonical).encode(), 'Markdown drift')
|
||||||
|
|
||||||
|
|
||||||
|
def markdown(d):
|
||||||
|
lines = ['# P19-03 到達表', '', d['acceptance'], '', d['identity_policy'], '',
|
||||||
|
'共通原本ルールと人物固有欠損を分離する。implementedは狭い保持処理だけで、自然ルート完成ではない。', '',
|
||||||
|
'|人物ID|原名|ルート|現在の到達性|', '|---|---|---|---|']
|
||||||
|
for c in d['cells']:
|
||||||
|
lines.append('|%s|%s|%s|欠損・通常到達未証明|' % (c['character_id'], c['name'], c['route']))
|
||||||
|
lines += ['', '## 共通原本ルール', '']
|
||||||
|
for r in d['common_rules']:
|
||||||
|
lines += ['### ' + r['id'], '', r['status'] + ' / owner: ' + r['owner'], '',
|
||||||
|
r['observation'], '', '原本 %s:%s–%s' % (BASELINE, *r['authority']['lines']), '',
|
||||||
|
'```text', r['authority']['text'], '```', '',
|
||||||
|
'実装証拠: ' + json.dumps(r['implementation'], ensure_ascii=False, sort_keys=True), '']
|
||||||
|
if 'legacy_evidence' in r:
|
||||||
|
lines += ['旧実装: ' + r['legacy_classification'] + ' / ' + r['legacy_reason'], '',
|
||||||
|
'旧証拠 (git_blob_oid: Git blob OID; sha256: file-bytes SHA-256): ' +
|
||||||
|
json.dumps(r['legacy_evidence'], ensure_ascii=False, sort_keys=True), '']
|
||||||
|
for c in d['cells']:
|
||||||
|
lines += ['## ' + c['character_id'] + ' / ' + c['name'] + ' / ' + c['route'], '']
|
||||||
|
for category, record in c['categories'].items():
|
||||||
|
g = record['character_specific']
|
||||||
|
lines += ['- ' + category + ': 共通 [' + ', '.join(record['common_rule_refs']) +
|
||||||
|
'] / missing / ' + g['gap'] + ' / owner: ' + g['owner'] +
|
||||||
|
' / 原本 ' + str(g['authority']['lines']) + '・人物 ' + str(g['character_authority']['lines'])]
|
||||||
|
if category == 'interruption_cancellation':
|
||||||
|
lines += [' 原本中断参照: ' + json.dumps(record['route_termination_authorities'], ensure_ascii=False),
|
||||||
|
' 旧確定ロック legacy_not_original: ' + json.dumps(record['legacy_evidence'], ensure_ascii=False)]
|
||||||
|
lines.append('')
|
||||||
|
lines += ['## ルート別適用性', '']
|
||||||
|
for route, support in d['route_support'].items():
|
||||||
|
lines += ['### ' + route, '']
|
||||||
|
lines += ['- ' + k + ': ' + v for k, v in support.items() if isinstance(v, str)]
|
||||||
|
lines += ['- 欠損機構: ' + ', '.join(support['missing_mechanisms']), '']
|
||||||
|
lines += ['D05/D08、閾値・率・初期値はpending。完全な出典本文・SHA・原子要件全レコードはJSONに保持する。', '', d['accounting'], '']
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
p = argparse.ArgumentParser(description=__doc__)
|
||||||
|
p.add_argument('--project-root', type=Path, required=True)
|
||||||
|
mode = p.add_mutually_exclusive_group(required=True)
|
||||||
|
mode.add_argument('--check', action='store_true')
|
||||||
|
mode.add_argument('--write', action='store_true')
|
||||||
|
mode.add_argument('--source-binding-only', action='store_true')
|
||||||
|
args = p.parse_args()
|
||||||
|
try:
|
||||||
|
root = args.project_root.resolve()
|
||||||
|
result = binding(root)
|
||||||
|
if not args.source_binding_only:
|
||||||
|
canonical = expected(root)
|
||||||
|
validate_legacy(root, canonical)
|
||||||
|
if args.write:
|
||||||
|
(root / OUT / 'REACHABILITY_TABLE.json').write_text(json.dumps(canonical, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
|
||||||
|
(root / OUT / 'REACHABILITY_TABLE.md').write_text(markdown(canonical), encoding='utf-8')
|
||||||
|
else:
|
||||||
|
validate_legacy(root, decode((root / OUT / 'REACHABILITY_TABLE.json').read_bytes()))
|
||||||
|
verify_records(decode((root / OUT / 'REACHABILITY_TABLE.json').read_bytes()), canonical,
|
||||||
|
(root / OUT / 'REACHABILITY_TABLE.md').read_bytes())
|
||||||
|
print(json.dumps({'audit_integrity': 'pass', 'formal_acceptance': 'pending_runner_and_independent_review', **result}))
|
||||||
|
return 0
|
||||||
|
except (ValueError, OSError, KeyError, TypeError, subprocess.CalledProcessError) as e:
|
||||||
|
print(json.dumps({'audit_integrity': 'fail', 'error': str(e)}))
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
+167
@@ -0,0 +1,167 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Meaningful in-memory controls; no product, Git or fixture writes."""
|
||||||
|
import argparse
|
||||||
|
import copy
|
||||||
|
from pathlib import Path
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
import check_reachability_table as audit
|
||||||
|
|
||||||
|
ROOT = None
|
||||||
|
|
||||||
|
|
||||||
|
class AuditTests(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.canonical = audit.expected(ROOT)
|
||||||
|
|
||||||
|
def reject(self, mutate):
|
||||||
|
changed = copy.deepcopy(self.canonical)
|
||||||
|
mutate(changed)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
audit.verify(changed, self.canonical)
|
||||||
|
|
||||||
|
def test_valid_implemented_partial_missing(self):
|
||||||
|
# All three legitimate status classes must survive serialization and the
|
||||||
|
# exact same verifier used by --check, without status promotion.
|
||||||
|
import json
|
||||||
|
value = audit.decode(json.dumps(self.canonical))
|
||||||
|
self.assertEqual({r['status'] for r in value['common_rules']}, {'implemented', 'partial', 'missing'})
|
||||||
|
audit.verify(value, self.canonical)
|
||||||
|
|
||||||
|
def test_independent_legacy_source_evidence(self):
|
||||||
|
import subprocess
|
||||||
|
document = audit.decode((ROOT / audit.OUT / 'REACHABILITY_TABLE.json').read_bytes())
|
||||||
|
audit.validate_legacy(ROOT, document)
|
||||||
|
for rule in document['common_rules']:
|
||||||
|
for entry in rule.get('legacy_evidence', []):
|
||||||
|
data = subprocess.check_output(['git', 'show', audit.ANCHOR + ':' + entry['path']], cwd=ROOT)
|
||||||
|
self.assertEqual(data, (ROOT / entry['path']).read_bytes())
|
||||||
|
self.assertEqual(entry['git_blob_oid'], subprocess.check_output(
|
||||||
|
['git','hash-object','--stdin'], input=data, cwd=ROOT).decode().strip())
|
||||||
|
start, end = entry['lines']
|
||||||
|
self.assertEqual(entry['text'], '\n'.join(data.decode().splitlines()[start-1:end]))
|
||||||
|
rules = {r['id']: r for r in document['common_rules']}
|
||||||
|
self.assertIn('func restore_shield', rules['love_actions']['legacy_evidence'][0]['text'])
|
||||||
|
self.assertIn('shield_broken', rules['zero_break']['legacy_evidence'][0]['text'])
|
||||||
|
self.assertIn('EXCLUSIVE_LOCKED', rules['threshold_route_loss']['legacy_evidence'][1]['text'])
|
||||||
|
|
||||||
|
def test_legacy_evidence_required_independently(self):
|
||||||
|
for identity in audit.LEGACY_RANGES:
|
||||||
|
for mutation in ('drop', 'empty', 'absence', 'blob', 'text'):
|
||||||
|
changed = copy.deepcopy(self.canonical)
|
||||||
|
rule = next(r for r in changed['common_rules'] if r['id'] == identity)
|
||||||
|
if mutation == 'drop':
|
||||||
|
rule.pop('legacy_evidence')
|
||||||
|
elif mutation == 'empty':
|
||||||
|
rule['legacy_evidence'] = []
|
||||||
|
elif mutation == 'absence':
|
||||||
|
rule['implementation'] = 'not implemented'
|
||||||
|
else:
|
||||||
|
rule['legacy_evidence'][0]['git_blob_oid' if mutation == 'blob' else 'text'] = 'stale'
|
||||||
|
with self.subTest(identity=identity, mutation=mutation), self.assertRaises(ValueError):
|
||||||
|
audit.validate_legacy(ROOT, changed)
|
||||||
|
|
||||||
|
def test_exact_cell_product(self):
|
||||||
|
mutations = {
|
||||||
|
'missing': lambda d: d['cells'].pop(),
|
||||||
|
'duplicate': lambda d: d['cells'].append(copy.deepcopy(d['cells'][0])),
|
||||||
|
'wrong_character': lambda d: d['cells'][0].update(character_id='unregistered'),
|
||||||
|
'wrong_route': lambda d: d['cells'][0].update(route='unregistered'),
|
||||||
|
}
|
||||||
|
for name, mutate in mutations.items():
|
||||||
|
with self.subTest(name=name):
|
||||||
|
self.reject(mutate)
|
||||||
|
|
||||||
|
def test_categories_and_citations(self):
|
||||||
|
self.reject(lambda d: d['cells'][0]['categories'].pop('save_handling'))
|
||||||
|
self.reject(lambda d: d['common_rules'][0].pop('authority'))
|
||||||
|
self.reject(lambda d: d['cells'][0]['categories']['state_conditions']['character_specific'].pop('character_authority'))
|
||||||
|
|
||||||
|
def test_stale_record_evidence(self):
|
||||||
|
for field in ('text', 'baseline_sha256'):
|
||||||
|
with self.subTest(field=field):
|
||||||
|
self.reject(lambda d: d['common_rules'][0]['authority'].update({field: 'stale'}))
|
||||||
|
for field in ('text', 'sha256', 'anchor'):
|
||||||
|
with self.subTest(field=field):
|
||||||
|
self.reject(lambda d: d['common_rules'][0]['implementation'].update({field: 'stale'}))
|
||||||
|
|
||||||
|
def test_real_source_and_baseline_changes(self):
|
||||||
|
original = Path.read_bytes
|
||||||
|
for target in (audit.BASELINE, audit.STATE):
|
||||||
|
def corrupt(path):
|
||||||
|
value = original(path)
|
||||||
|
return value + b'\nchanged' if path == ROOT / target else value
|
||||||
|
with self.subTest(target=target), patch.object(Path, 'read_bytes', corrupt):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
audit.expected(ROOT)
|
||||||
|
actual_blob = audit.blob
|
||||||
|
def corrupt_blob(root, commit, path):
|
||||||
|
value = actual_blob(root, commit, path)
|
||||||
|
return value + b'\nchanged' if path == audit.STATE else value
|
||||||
|
with patch.object(audit, 'blob', side_effect=corrupt_blob):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
audit.expected(ROOT)
|
||||||
|
|
||||||
|
def test_no_invented_values_or_completion(self):
|
||||||
|
self.reject(lambda d: d['pending'].update(thresholds=50))
|
||||||
|
self.reject(lambda d: d['common_rules'][5].update(status='implemented'))
|
||||||
|
self.reject(lambda d: d['cells'][0].update(natural_route_complete=True))
|
||||||
|
self.reject(lambda d: d['cells'][0]['categories']['entry_required_events']['character_specific'].update(status='implemented', gap=None))
|
||||||
|
|
||||||
|
def test_owner_and_status(self):
|
||||||
|
self.reject(lambda d: d['common_rules'][0].update(owner='P99-01'))
|
||||||
|
self.reject(lambda d: d['common_rules'][0].update(owner='P19-02'))
|
||||||
|
self.reject(lambda d: d['common_rules'][0].update(status='complete'))
|
||||||
|
|
||||||
|
def test_json_and_types(self):
|
||||||
|
for data in ('{', '{"x":1,"x":2}', '{"x":NaN}'):
|
||||||
|
with self.subTest(data=data), self.assertRaises(ValueError):
|
||||||
|
audit.decode(data)
|
||||||
|
for value in ([], None, 'table'):
|
||||||
|
with self.subTest(value=value), self.assertRaises(ValueError):
|
||||||
|
audit.verify(value, self.canonical)
|
||||||
|
self.reject(lambda d: d.update(cells={}))
|
||||||
|
self.reject(lambda d: d.update(schema_version=True))
|
||||||
|
|
||||||
|
def test_markdown_drift(self):
|
||||||
|
expected = audit.markdown(self.canonical).encode()
|
||||||
|
audit.verify_records(self.canonical, self.canonical,
|
||||||
|
(ROOT / audit.OUT / 'REACHABILITY_TABLE.md').read_bytes())
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
audit.verify_records(self.canonical, self.canonical, expected + b'\nchanged')
|
||||||
|
|
||||||
|
def test_full_frozen_map_and_identities(self):
|
||||||
|
audit.binding(ROOT) # Valid frozen-map control; no native execution.
|
||||||
|
actual_selection = audit.selection(ROOT)
|
||||||
|
for kind in ('missing', 'new', 'changed'):
|
||||||
|
changed = copy.deepcopy(actual_selection)
|
||||||
|
key = next(iter(changed))
|
||||||
|
if kind == 'missing':
|
||||||
|
del changed[key]
|
||||||
|
elif kind == 'new':
|
||||||
|
changed['scripts/invented.gd'] = {'bytes': 0, 'sha256': '0' * 64}
|
||||||
|
else:
|
||||||
|
changed[key]['sha256'] = '0' * 64
|
||||||
|
with self.subTest(kind=kind), patch.object(audit, 'selection', return_value=changed):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
audit.binding(ROOT)
|
||||||
|
original = Path.read_bytes
|
||||||
|
pins = audit.decode(original(ROOT / audit.OUT / 'FROZEN_GAME_INPUT_PINS.json'))
|
||||||
|
targets = [ROOT / audit.OUT / 'FROZEN_GAME_INPUT_PINS.json', ROOT / pins['marker_contract_path'],
|
||||||
|
ROOT / pins['policy_path'], Path(pins['engine_path'])]
|
||||||
|
for target in targets:
|
||||||
|
def corrupt(path):
|
||||||
|
value = original(path)
|
||||||
|
return value + b'\nchanged' if path == target else value
|
||||||
|
with self.subTest(target=str(target)), patch.object(Path, 'read_bytes', corrupt):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
audit.binding(ROOT)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument('--project-root', type=Path, required=True)
|
||||||
|
args, rest = parser.parse_known_args()
|
||||||
|
ROOT = args.project_root.resolve()
|
||||||
|
unittest.main(argv=[__file__] + rest)
|
||||||
Reference in New Issue
Block a user