P19-03-TABLE: P19-03 分岐の到達性:原7名×3ルートの全到達表と未制作欠損監査
This commit is contained in:
+52
-2
@@ -4,7 +4,7 @@
|
||||
|
||||
## 変更範囲
|
||||
|
||||
修復開始点e0420d57a381a1644357f0590536e1d47c9bca9fから、このディレクトリの指定5ファイルのみを変更した。原本、原子要件、work packages、decisions、TASK_SPEC、選定記録、frozen pins、ゲーム入力、marker registryは変更していない。git書込み、他worker起動、Runner操作、ゲーム起動は行っていない。素材追加なし。
|
||||
Repair 1開始点e0420d57a381a1644357f0590536e1d47c9bca9fから、このディレクトリの指定5ファイルのみを変更した。原本、原子要件、work packages、decisions、TASK_SPEC、選定記録、frozen pins、ゲーム入力、marker registryは変更していない。git書込み、他worker起動、Runner操作、ゲーム起動は行っていない。素材追加なし。
|
||||
|
||||
## 要件と検査の対応
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
- 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とは区別する。
|
||||
|
||||
## 実行記録(今回のローカル確認)
|
||||
## Repair 1の実行記録(過去のローカル確認)
|
||||
|
||||
以下は正式Runner実行ではない。記録済み修復指示に基づき3登録コマンドをローカル実行した。
|
||||
|
||||
@@ -73,3 +73,53 @@ python3 -B /Users/korizatou114/Projects/TheTree-managed-worktrees/p19-03-reachab
|
||||
- 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が必要。どの完了数・元台帳も変更していない。
|
||||
|
||||
|
||||
## Repair 2: BASE定義projectionと台帳更新の分離
|
||||
|
||||
開始点Sは806de25068bc423136063791afcea811d251ee1b。編集前にACTUAL_OPUS_REPAIR2_DIRECTION.jsonとACTUAL_FAILED_INTEGRATION.json、immutable TASK_SPECと既存チェッカーを確認。最新run指示に従い、実マージ候補の作成・検証は実装担当では行わない。指定5ファイルのみを変更する。Repair 1の4修正を保持し、任意の低3指摘は延期したまま。
|
||||
|
||||
### 実差分とallowlistの根拠
|
||||
|
||||
git showによるBASE 8ca9263とmain 6e17caeのkey-level比較を行った。candidate 1875b03のwork_packages.jsonは6e17caeと完全一致。差分は次だけ(配列index 19=P19、remaining_deliverables index 2=P19-03):
|
||||
|
||||
|JSON path|差分|
|
||||
|---|---|
|
||||
|/19/current_summary|変更|
|
||||
|/19/updated_at_jst|変更|
|
||||
|/19/remaining_deliverables/2/current_execution|TASK_SPEC.jsonからROOT_CURRENT_ACCEPTANCE_STATUS.jsonへの参照更新|
|
||||
|/19/remaining_deliverables/2/current_summary|変更|
|
||||
|/19/remaining_deliverables/2/progress_summary|変更|
|
||||
|/19/remaining_deliverables/2/remaining_gaps/0, /1|通常integration/final待ちの進捗説明へ更新|
|
||||
|/19/remaining_deliverables/2/evidence|1件から2件|
|
||||
|/19/remaining_deliverables/2/submitted_commit|806de25068bc423136063791afcea811d251ee1bを追加|
|
||||
|/19/remaining_deliverables/2/updated_at_jst|変更|
|
||||
|
||||
package allowlistはstate / legacy_state / current_summary / updated_at_jst。unit allowlistはstate / current_execution / remaining_gaps / updated_at_jst / submitted_commit / current_summary / progress_summary / evidence。state系は通常進捗遷移として明示的に許可し、同じ実registerに既存のキーだけを採用した。通常finalの完了時刻・source-ref用に、BASEの他unitで既に使われるcompleted_at_jst(P13-03)、implementation_commit(P13-03)、integrated_commit(P13-02)、integration_commit(P13-03)、normal_integration_candidate(P04-01)だけをunit階層で追加許可した。キーのprefixや任意のtimestamp/source_refキーは許可しない。allowlistの値は進捗メタデータとしてprojectionから全体を除外する。
|
||||
|
||||
それ以外の全キーは未知キーを含め再帰的な値・型・有無を保ち、BASE projectionと比較する。30 package / 88 unitのID・順序・件数、全title/work/acceptance、phase/parent/depends_on/requirement_ids/decisions/supplement_ids/owner_roles/state_scope等を固定する。atomic_requirements.jsonとdecisions.jsonは実main差分なしのため引き続きbyte-exact。D05/D08のstatus/answerは変更不可。selection recordもbyte-exact。
|
||||
|
||||
所有者集合・原子要件・authority_snapshotsはBASE blobのみから読む。各snapshotはanchored_base_commit、BASE blobのsha256、projected recordsを保持する。work packagesは全30/88定義を保持し、進捗を除外した。表の21セル、共通ルール・引用・status・owner・欠損・pending/null値は変更していない。Markdownには固定参照の説明だけを追加した。--writeは1回実行、--checkは読込み専用。
|
||||
|
||||
### Repair 2 ローカル実行
|
||||
|
||||
cwdとproject-rootは /Users/korizatou114/Projects/TheTree-managed-worktrees/p19-03-reachability/P19-03-TABLE-implement。
|
||||
以下は正式Runner試験ではない。実際の呼出しは各scriptをdocs/plans/original-completion-2026-10-02/execution/p19-03/配下の相対パスで指定し、--project-rootには上記実絶対パスを指定した。
|
||||
|
||||
- python3 -B 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: exit 0。
|
||||
- p19-table: python3 -B 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: exit 0。
|
||||
- p19-source-binding: python3 -B 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: exit 0。
|
||||
|
||||
上記3呼出しのstdoutはaudit_integrity=pass、formal_acceptance=pending_runner_and_independent_review、actual_commit=806de25068bc423136063791afcea811d251ee1b、selected_files=1368、anchor=1b07b87d3736e777932315a4bdde8b70cbf9cb23、file_equality=true、native_evidence_reuse_accepted=false。spec file-bytesとRunner contractの両hashも従来値のまま。
|
||||
|
||||
実mainおよび失敗candidateの実blobをPath.read_bytes差替えで読ませる自己試験は、生成JSONとMarkdownのbyte完全一致を検証する。実registerの書換え・一時worktree・mergeは不要。合成完了/source-ref例も同じ検査を通す。各定義フィールドの変更・削除、package/unit objectの未知キー追加、package/unit/各定義listの件数/順序変更、D05/D08承認、atomic/decisions/selectionの変更を個別subtestで拒否する。既存12テストは維持。
|
||||
|
||||
実merged-candidate確認: **Root preflight pending**。新S提出と通常validation後、Rootが最新main上の実Sについてラベル付き非正式isolated preflightで同じ3チェッカーを実行する。その後に別Opus whole-original re-review、通常prepare-integration/finalが必要。今回のローカル成功やworker exit 0は受入ではない。正式進捗14/88は維持。native GUI/Windowsは本単位に適用外、global Windowsとゲーム受入はopen。
|
||||
|
||||
自己試験の探索版2回は全recordや深い履歴データの同型フィールドまで反復走査して時間がかかったため中断(各exit 130、KeyboardInterrupt)。package/unitの異なる定義キーごとに変更・削除を検査し、ネストした定義値は全体を比較する形へ整理した。BASE blobを一度実読込して負例ループ内で再利用し、各負例ごとのgit show起動を省いた。検査対象・既存controlは削除していない。shell here-documentを用いた初回の読取り差分確認は環境の一時ファイル制約で失敗(exit 1)、python3 -cで同じ確認を実行した。
|
||||
|
||||
最終p19-negative呼出し: python3 -B 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
|
||||
|
||||
exit 0、出力: Ran 16 tests in 16.285s / OK。実main/candidate正例、合成final正例、全package/unit定義フィールド負例と既存controlを含む。git diff --checkもexit 0。最終差分は指定5ファイルのみ。生成前後のJSONを比較しauthority_snapshots以外の全フィールドが一致することも静的確認した。
|
||||
|
||||
同じp19-tableコマンドを絶対scriptパスで再確認(exit 0、同じpass出力)。呼出し前後にこのディレクトリの全通常ファイルSHA-256 mapを比較し一致: Read-only byte determinism: pass。最終git diff --checkもexit 0。
|
||||
|
||||
+9331
-139
File diff suppressed because it is too large
Load Diff
@@ -688,6 +688,8 @@ missing / owner: P19-02
|
||||
- Windows: 未受入: global same-Windows acceptanceはopen。文書単位のnative GUI/Windows実行は適用外。
|
||||
- 欠損機構: defense_reduction, post_break_metric, barrier_block, coassignment_attack, zero_break, contact_after_break, threshold_route_loss, duel_types, duel_win, duel_loss, love_actions, ntr_actions, corruption_actions, history_fields, history_cycle_parentage, portrait_marks, route_skills, departure
|
||||
|
||||
Authority snapshots: anchored_base_commit=8ca9263fee1ec4374d97c01fda82b95e1df6a977; JSON retains anchored blob SHA-256 and projected definition records.
|
||||
|
||||
D05/D08、閾値・率・初期値はpending。完全な出典本文・SHA・原子要件全レコードはJSONに保持する。
|
||||
|
||||
14/88 unchanged; formal Runner validation, independent whole-unit Opus review and normal integration/final required; no game acceptance
|
||||
|
||||
+50
-8
@@ -124,11 +124,50 @@ def binding(root):
|
||||
'Runner_git_show_stripped_UTF8_contract_spec_sha256': RUNNER_CONTRACT_SPEC_SHA256}
|
||||
|
||||
|
||||
# Exact key-level BASE -> 6e17cae/1875b03 diff; no prefix/wildcard exclusions.
|
||||
PACKAGE_MUTABLE = frozenset(("state", "legacy_state", "current_summary", "updated_at_jst"))
|
||||
UNIT_MUTABLE = frozenset((
|
||||
"state", "current_execution", "remaining_gaps", "updated_at_jst",
|
||||
"submitted_commit", "current_summary", "progress_summary", "evidence",
|
||||
# Normal final/source references: these names already occur in BASE units.
|
||||
"completed_at_jst", "implementation_commit", "integrated_commit",
|
||||
"integration_commit", "normal_integration_candidate",
|
||||
))
|
||||
|
||||
|
||||
def definition_projection(register):
|
||||
require(type(register) is list, "package register must be a list")
|
||||
projected = []
|
||||
for package in register:
|
||||
require(type(package) is dict, "package must be an object")
|
||||
record = {k: v for k, v in package.items() if k not in PACKAGE_MUTABLE}
|
||||
units = record.get("remaining_deliverables")
|
||||
require(type(units) is list, "deliverables must be a list")
|
||||
require(all(type(unit) is dict for unit in units), "unit must be an object")
|
||||
record["remaining_deliverables"] = [
|
||||
{k: v for k, v in unit.items() if k not in UNIT_MUTABLE} for unit in units]
|
||||
projected.append(record)
|
||||
return projected
|
||||
|
||||
|
||||
def validate_register(root, name, current):
|
||||
anchored = blob(root, BASE, PLAN + name)
|
||||
if name == "work_packages.json":
|
||||
# JSON serialization preserves types (True must not compare equal to 1).
|
||||
encode = lambda value: json.dumps(definition_projection(decode(value)),
|
||||
ensure_ascii=False, sort_keys=True)
|
||||
require(encode(current) == encode(anchored),
|
||||
"original definition changed: " + name)
|
||||
else:
|
||||
require(current == anchored, "original register changed: " + name)
|
||||
return anchored
|
||||
|
||||
|
||||
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)
|
||||
anchored = {name: validate_register(root, name, raw(PLAN + name))
|
||||
for name in ('atomic_requirements.json', 'work_packages.json', 'decisions.json')}
|
||||
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()
|
||||
@@ -183,9 +222,9 @@ def expected(root):
|
||||
['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 = decode(anchored['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', [])}
|
||||
owners = {x['id'] for p in decode(anchored['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:
|
||||
@@ -226,10 +265,11 @@ def expected(root):
|
||||
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')},
|
||||
'authority_snapshots': {PLAN + name: {
|
||||
'sha256': sha(data), 'anchored_base_commit': BASE,
|
||||
'records': definition_projection(decode(data)) if name == 'work_packages.json'
|
||||
else decode(data) if name == 'decisions.json' else atoms}
|
||||
for name, data in anchored.items()},
|
||||
'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を原本ルートとしない。',
|
||||
@@ -289,6 +329,8 @@ def markdown(d):
|
||||
lines += ['### ' + route, '']
|
||||
lines += ['- ' + k + ': ' + v for k, v in support.items() if isinstance(v, str)]
|
||||
lines += ['- 欠損機構: ' + ', '.join(support['missing_mechanisms']), '']
|
||||
lines += ['Authority snapshots: anchored_base_commit=' + BASE +
|
||||
'; JSON retains anchored blob SHA-256 and projected definition records.', '']
|
||||
lines += ['D05/D08、閾値・率・初期値はpending。完全な出典本文・SHA・原子要件全レコードはJSONに保持する。', '', d['accounting'], '']
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
+117
@@ -21,6 +21,123 @@ class AuditTests(unittest.TestCase):
|
||||
with self.assertRaises(ValueError):
|
||||
audit.verify(changed, self.canonical)
|
||||
|
||||
def register_output(self, data):
|
||||
original = Path.read_bytes
|
||||
def read(path):
|
||||
return data if path == ROOT / audit.PLAN / "work_packages.json" else original(path)
|
||||
with patch.object(Path, "read_bytes", read):
|
||||
document = audit.expected(ROOT)
|
||||
import json
|
||||
self.assertEqual(json.dumps(document, ensure_ascii=False, indent=2) + "\n",
|
||||
(ROOT / audit.OUT / "REACHABILITY_TABLE.json").read_text())
|
||||
self.assertEqual(audit.markdown(document).encode(),
|
||||
(ROOT / audit.OUT / "REACHABILITY_TABLE.md").read_bytes())
|
||||
|
||||
def test_actual_main_and_candidate_metadata(self):
|
||||
for commit in ("6e17cae", "1875b03"):
|
||||
with self.subTest(commit=commit):
|
||||
self.register_output(audit.blob(ROOT, commit, audit.PLAN + "work_packages.json"))
|
||||
|
||||
def test_final_ledger_metadata(self):
|
||||
import json
|
||||
register = audit.decode(audit.blob(ROOT, audit.BASE, audit.PLAN + "work_packages.json"))
|
||||
package = next(p for p in register if p["id"] == "P19")
|
||||
unit = next(u for u in package["remaining_deliverables"] if u["id"] == "P19-03")
|
||||
package.update(state="completed", legacy_state="completed",
|
||||
current_summary="ledger completion", updated_at_jst="2026-10-17")
|
||||
unit.update({key: "synthetic final reference" for key in audit.UNIT_MUTABLE})
|
||||
unit["evidence"] = [{"path": "docs/final.json", "sha256_at_integration": "0" * 64}]
|
||||
unit["remaining_gaps"] = []
|
||||
self.register_output(json.dumps(register).encode())
|
||||
|
||||
def test_every_register_definition_field(self):
|
||||
import json
|
||||
base = audit.decode(audit.blob(ROOT, audit.BASE, audit.PLAN + "work_packages.json"))
|
||||
# Independently mutate/remove every package and unit definition field.
|
||||
# Compare full nested values; unknown additions fail at both record levels.
|
||||
def visit(value, path=(), level="packages"):
|
||||
if isinstance(value, list):
|
||||
yield path, "count"
|
||||
if len(value) > 1:
|
||||
yield path, "order"
|
||||
for i, child in enumerate(value):
|
||||
yield from visit(child, path + (i,),
|
||||
"package" if not path else "unit" if path[-1] == "remaining_deliverables" else "nested")
|
||||
elif isinstance(value, dict):
|
||||
yield path, "unknown"
|
||||
excluded = audit.PACKAGE_MUTABLE if level == "package" else audit.UNIT_MUTABLE if level == "unit" else ()
|
||||
for key, child in value.items():
|
||||
if key in excluded:
|
||||
continue
|
||||
yield path + (key,), "change"
|
||||
yield path + (key,), "remove"
|
||||
if key == "remaining_deliverables":
|
||||
yield from visit(child, path + (key,), "units")
|
||||
elif isinstance(child, list):
|
||||
yield path + (key,), "count"
|
||||
if len(child) > 1 and child[0] != child[1]:
|
||||
yield path + (key,), "order"
|
||||
|
||||
seen = set()
|
||||
anchored = audit.blob(ROOT, audit.BASE, audit.PLAN + "work_packages.json")
|
||||
for path, operation in visit(base):
|
||||
# Each distinct definition key/structural path once; repeated records
|
||||
# use the same schema, so do not pad by testing it 30/88 times.
|
||||
shape = tuple("*" if type(part) is int else part for part in path)
|
||||
if (shape, operation) in seen:
|
||||
continue
|
||||
seen.add((shape, operation))
|
||||
changed = copy.deepcopy(base)
|
||||
target = changed
|
||||
for key in path[:-1]:
|
||||
target = target[key]
|
||||
value = target[path[-1]] if path else changed
|
||||
if operation == "change":
|
||||
target[path[-1]] = {"altered_definition": True}
|
||||
elif operation == "remove":
|
||||
del target[path[-1]]
|
||||
elif operation == "unknown":
|
||||
value["unknown_metadata"] = "must fail"
|
||||
elif operation == "count":
|
||||
value.append({})
|
||||
else:
|
||||
value[0], value[1] = value[1], value[0]
|
||||
with self.subTest(path=path, operation=operation), patch.object(audit, 'blob', return_value=anchored), self.assertRaises(ValueError):
|
||||
audit.validate_register(ROOT, "work_packages.json", json.dumps(changed).encode())
|
||||
|
||||
def test_atomic_decision_and_selection_integrity(self):
|
||||
import json
|
||||
for name in ("atomic_requirements.json", "decisions.json"):
|
||||
data = audit.blob(ROOT, audit.BASE, audit.PLAN + name)
|
||||
with self.subTest(name=name), self.assertRaises(ValueError):
|
||||
audit.validate_register(ROOT, name, data + b" ")
|
||||
changed = audit.decode(data)
|
||||
if name == "atomic_requirements.json":
|
||||
changed["requirements"][0]["id"] = "changed_atomic_requirement"
|
||||
with self.assertRaises(ValueError):
|
||||
audit.validate_register(ROOT, name, json.dumps(changed).encode())
|
||||
decisions = audit.decode(audit.blob(ROOT, audit.BASE, audit.PLAN + "decisions.json"))
|
||||
def flip(value, identity):
|
||||
if isinstance(value, dict):
|
||||
if value.get("id") == identity:
|
||||
value["status"] = "approved"
|
||||
return True
|
||||
return any(flip(v, identity) for v in value.values())
|
||||
if isinstance(value, list):
|
||||
return any(flip(v, identity) for v in value)
|
||||
return False
|
||||
for identity in ("D05", "D08"):
|
||||
changed = copy.deepcopy(decisions)
|
||||
self.assertTrue(flip(changed, identity))
|
||||
with self.subTest(identity=identity), self.assertRaises(ValueError):
|
||||
audit.validate_register(ROOT, "decisions.json", json.dumps(changed).encode())
|
||||
original = Path.read_bytes
|
||||
target = ROOT / audit.OUT / "ACTUAL_OPUS_NEXT_ORIGINAL_UNIT_SELECTION.json"
|
||||
def corrupt(path):
|
||||
return original(path) + (b" " if path == target else b"")
|
||||
with patch.object(Path, "read_bytes", corrupt), self.assertRaises(ValueError):
|
||||
audit.expected(ROOT)
|
||||
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user