P26-01-LEDGER: 原P26-01 全必要内容の台帳
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,64 @@
|
|||||||
|
# P26-01 全必要内容の台帳 — 実装提出
|
||||||
|
|
||||||
|
対象: P26-01 / AC-P26-01。原作業「全章、7名の3ルート、全エンド、台詞、立ち絵、音声の必要枠と条件を確定する。」、原受入「原本で要求された未制作内容を欠損として記録し、架空の完成数を置かない。」を変更しない。
|
||||||
|
|
||||||
|
本提出は内容監査資料であり、原本全体は30パッケージ・88単位、正式完了15/88のまま、今回加算0。P26-01の自己受入は行わない。独立したwhole-unit Opusレビューが原受入を判定する。
|
||||||
|
|
||||||
|
## 作成した5成果物
|
||||||
|
|
||||||
|
|成果物|役割|
|
||||||
|
|---|---|
|
||||||
|
|CONTENT_LEDGER.json|原文引用、条件、状態、欠損、責任先、実ソースとSHAを持つ監査台帳|
|
||||||
|
|CONTENT_LEDGER.md|JSONの全行と固定参照情報を欠落させない決定的な表示|
|
||||||
|
|check_content_ledger.py|stdlibの読取専用検査、source binding、明示的な生成モード|
|
||||||
|
|test_content_ledger.py|正例と、原本要求欠落・架空数・誤完成・出典改変等の負例|
|
||||||
|
|IMPLEMENTATION_REPORT.md|原単位全体との対応と残存不足、本報告|
|
||||||
|
|
||||||
|
変更は契約の5 allowed_paths内だけ。TASK_SPEC、selection、frozen pins、原本、計画定義、決定、修正回数は変更しない。git書込み、他エージェント、Runner操作、ゲーム・素材編集、API/安全性/対象範囲変更は行わない。新規素材の導入はない。
|
||||||
|
|
||||||
|
## 原単位全体の対応
|
||||||
|
|
||||||
|
原本は `docs/traceability/original/BASELINE.txt`、SHA256 `75e4bdbdeb379c9269553f0c776bdb9cc96e01f2d6aa5ca650d132c63a8aeb1a`。各行は行番号、原文全文、ファイルSHAを保持する。
|
||||||
|
|
||||||
|
|要求|台帳ID/根拠|現状と不足の所有先|
|
||||||
|
|---|---|---|
|
||||||
|
|全章|chapters.all、原本8・26〜30・256行|メインストーリーの要求のみ。章名・章数・順序・場面一覧は未定義。仕様書の第1〜14章をシナリオ章に転用しない。D08|
|
||||||
|
|7名×3ルート|route.anri/fine/tomari/amelia/liz/mimi/satsuki × love/ntr/corruption、118〜124・30・132・138〜161行|アンリ、フィーネ、トマリ、アメリア、リズ、ミミ、サツキと純愛/NTR/堕落の正確な対応。人物固有のイベント列、入口・分岐・終点・台詞・素材・履歴/保存・通常到達は欠損。P26-02 / D08|
|
||||||
|
|全エンド|endings.all / endings.bad / endings.game_over、9・135・72行|全エンド集合、特定バッドエンド、赤字決算3回連続のゲームオーバーを区別。個別終点ID・本数は未定義。3ルートを3個の完成エンドに数えない。D08 / P26-02|
|
||||||
|
|台詞|dialogue.all、155・230・256行、全route行の条件|会話/イベント/脚本の必要枠。台詞本文、話者一覧、行数、シーン列は未定義。UI文言や汎用関係イベントを完成脚本に数えない。P26-02 / D08|
|
||||||
|
|立ち絵|portraits.all とP22人物利用枠、160〜161・230・235・242行|主/サブ、表情、衣装、破損、汚れ、刻印、妊婦外見、ルート別描き分け。必要枚数・共有方針は未定義。P26-03 / D09|
|
||||||
|
|音声|voice.all とP22人物voice枠、252・257行|ボイス収録・実装が欠損。話者/台詞/クリップ数/担当未定義。BGM、UI音、汎用音はボイスの代用にしない。P26-03 / D09|
|
||||||
|
|図鑑|encyclopedia.all、9行|対象集合、ID、解放条件、項目数は未定義。D08|
|
||||||
|
|横断する必要内容|clause.*、原本27〜34・132・134〜135・138〜161・229〜230・234〜235・242〜246・252・255〜258行|原本共通ルール、世界設定、CG/差分/モブ、SDモーション、制作段階を保存。今回新規の物語・台詞・場面は制作しない。P26-02/03、P22制作系とD08/D09|
|
||||||
|
|
||||||
|
台帳の必要数・完成数はnull(未定義/未計数)。既知の7名×3ルートと、既存P22の614利用枠/用途群は、画像枚数、ボイス数、制作完了数、原子分母への換算をしない。未定義状態も欠損として明示する。状態語彙は produced / placeholder / code-drawing / partial / missing / undefined。完成を裏付ける証拠がないためproducedへの昇格はない。
|
||||||
|
|
||||||
|
## 版と実証拠
|
||||||
|
|
||||||
|
計画定義の基準は `379a1ba8fd25b5ad24226178a2492f91761dc31d`、ゲーム監査アンカーは `f4f85bc54c39361f9efcd16f67a17835559ca70c`。TASK_SPEC raw SHA256は `8026040345de6b7d06cfce40beac9150bfe9ad303d6b26c11bd94e66b1080b29`、stripしたUTF-8表現は `1fedc9ade42a04722846daa9ac8ec26f821fb4efbce8dbbb53e3e76e31f38d02`。両表現を区別する。selectionは基準コミットの同一バイトで固定する。
|
||||||
|
|
||||||
|
P19-03 REACHABILITY_TABLE.jsonは読取専用の既存監査でありP26の前身ではない。21セルの人物名・ルート名・natural_route_complete=falseを確認する。台帳にはそのセル参照とファイルSHAを保存する。実ソースのカタログ、OriginalRelationshipState、Runtime identity検査、relationship_serviceの保存コンテナ、PixelPeopleの汎用描画も直接参照し、正確な行範囲・本文・SHAを保持する。専用の明示状態検証/保存があることと、人物別ルートの内容制作・到達がないことを区別する。Runtimeの主人公ID `player` もこの証拠と全入力固定の対象。
|
||||||
|
|
||||||
|
P22-01は既存の360施設枠・176人物用途群・76その他用途群・2状態群を全て参照する。原レコードは型を保持して保存し、現在の表現先の実ファイルSHAを別に取得する。過去のSHAと現在のSHAが異なる場合も両方を残し、旧レコードを書き換えない。既存296mediaの全現物参照と、固定入力中の全assetsパス/SHAも記録する。汎用コード描画、仮素材、完成素材を混同しない。現物の存在だけで用途・差分の品質/権利受入済みとは判定しない。
|
||||||
|
|
||||||
|
P22-01の元artifactsパスは非追跡アーカイブの別名であり、その入力はRootの公開コミット `137639773c9017f9ee7733d78e37179b687f965c` の `accepted-p22-01-inputs/` からも読める。公開manifest SHA256 `90b5a69d5f364012edabe5a68b8c7613f5f5ca47bc189d435dd2e69b8dd8c915` と全10ファイルのサイズ/SHAで固定する。元パスがある場合は公開バイトと一致必須、ない場合は固定git objectを使う。レビュー用別名の設置や新規ファイル追加は本workerでは行わない。P22-03 production_backlog.jsonはアンカー固定された未受入の下書き参考のみで、制作数量・承認・完了の権威にしない。
|
||||||
|
|
||||||
|
## チェッカーと正式試験
|
||||||
|
|
||||||
|
`--check` は台帳・出典・固定入力・決定的Markdownを読取専用で検査する。`--source-binding-only` は同じ出典結合と台帳を確認し、Markdown表示の一致を要求しない。`--write` は固定原文・実ソースから指定JSON/Markdownの2ファイルだけを生成する。JSON重複キーと非有限数値も拒否する。
|
||||||
|
|
||||||
|
元の30/88のacceptance/work/dependency/requirement等は基準版の計画定義へfail closedで結合し、明示した既存の進捗メタデータキーだけを除いて比較する。work_packages全体をバイト固定しない。atomic_requirementsとdecisionsは型/値を変えず基準版へ結合する。台帳は閉じたスキーマ相当の完全一致、ID一意性、原要求/利用枠集合、正確な人物/ルート、null数量、owner/status/type、引用とSHAを検査する。期待値は提出台帳から生成せず、固定原文の明示的な条項範囲・固定P19/P22・実ソースから作る。
|
||||||
|
|
||||||
|
負例は章/ルート/各エンド/台詞/立ち絵/音声/図鑑/共通カテゴリ/利用枠の欠落、架空数、欠損の完成昇格、owner/status/type/追加キー/重複ID、人物誤同定、原文/原文SHA/実証拠SHA/アンカー改変、不正JSON/重複キー/非有限値、Markdown drift、計画定義改変、公開manifest改変、選択入力の欠落/追加/変更/型改変を含む。正例は提出資料、独立期待集合、元の入力集合、許容される進捗メタデータ変更。
|
||||||
|
|
||||||
|
正式試験はRunnerが実行する。本workerは `--check`、`--source-binding-only`、負例テストを実行しておらず、合格は報告しない。JSON/Markdownは生成処理だけを実行した。正式コマンドは契約の p26-ledger / p26-negative / p26-source-binding の3本をそのまま使用する。
|
||||||
|
|
||||||
|
全1368のselected game input、settings、assets、tests、tools、marker、policy、engine、roster、329parseとdefault player identityをチェッカーで固定入力マップへ照合する。固定pin SHA256は `6c778226a4cc6d0b0d682500759e7d4229c926a27bd2e247b00b9c3e5f1a2fb0`。追加/欠落/変更のいずれも拒否し、全選択入力をf4f85bcのgit blobにも照合する。docs helperはゲーム選択集合外。
|
||||||
|
|
||||||
|
既存nativeの184 suites / 21903 checks / 329 parse / 186 owned-endedは履歴としてのみ保存する。既存証拠のsource S `a7e3069b167511b3194242b3403fbcc35244a526` とC `65f07bf5eaa759a8e2e17c7bdbbbdbceb24ee373` への適用性は独立ゲートであり、本提出はnative再利用受入を主張しない。新規のnative full runは行わない。
|
||||||
|
|
||||||
|
## 残存不足と受入境界
|
||||||
|
|
||||||
|
D05の既存未解決事項、D08の章/ルート/全エンド/図鑑/必要内容範囲、D09の品質/量/担当/予算/権利は未解決のまま。D05の既存承認を撤回も拡張もしない。P26-02/03と上流の制作・ゲームプレイ・保存/移行・素材受入は未完。held gestures、same-Windows、global acceptanceも未受入。本監査に製品操作は追加されない。
|
||||||
|
|
||||||
|
whole-unit独立レビューが未定義の記録では原受入を満たさないと判断した場合は、正直に不合格/無加算とする。必要な通常prepare-integration、exact C/M、normal final、Rootの台帳更新/main pushはworker範囲外であり未実行。これらと独立受入が終わるまでは16/88へ進めない。
|
||||||
@@ -0,0 +1,346 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""P26 documentation audit. Stdlib only; --check never writes or runs Godot."""
|
||||||
|
import argparse
|
||||||
|
from functools import lru_cache
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
OUT = 'docs/plans/original-completion-2026-10-02/execution/p26-01/'
|
||||||
|
PLAN = 'docs/plans/original-completion-2026-10-02/'
|
||||||
|
BASELINE = 'docs/traceability/original/BASELINE.txt'
|
||||||
|
BASE = '379a1ba8fd25b5ad24226178a2492f91761dc31d'
|
||||||
|
ANCHOR = 'f4f85bc54c39361f9efcd16f67a17835559ca70c'
|
||||||
|
BASE_HASH = '75e4bdbdeb379c9269553f0c776bdb9cc96e01f2d6aa5ca650d132c63a8aeb1a'
|
||||||
|
PINS_HASH = '6c778226a4cc6d0b0d682500759e7d4229c926a27bd2e247b00b9c3e5f1a2fb0'
|
||||||
|
SPEC_HASH = '1fedc9ade42a04722846daa9ac8ec26f821fb4efbce8dbbb53e3e76e31f38d02'
|
||||||
|
P22 = 'artifacts/original-completion-2026-10-02/p22-01-usage-contract-candidate-r02/'
|
||||||
|
PUBLISHED = '137639773c9017f9ee7733d78e37179b687f965c'
|
||||||
|
ARCHIVE = OUT + 'accepted-p22-01-inputs/'
|
||||||
|
MANIFEST_HASH = '90b5a69d5f364012edabe5a68b8c7613f5f5ca47bc189d435dd2e69b8dd8c915'
|
||||||
|
P19 = PLAN + 'execution/p19-03/REACHABILITY_TABLE.json'
|
||||||
|
BACKLOG = PLAN + 'execution/p22-03/production_backlog.json'
|
||||||
|
PEOPLE = [('anri', 'アンリ'), ('fine', 'フィーネ'), ('tomari', 'トマリ'),
|
||||||
|
('amelia', 'アメリア'), ('liz', 'リズ'), ('mimi', 'ミミ'), ('satsuki', 'サツキ')]
|
||||||
|
ROUTES = {'love': '純愛', 'ntr': 'NTR', 'corruption': '堕落'}
|
||||||
|
STATUSES = {'produced', 'placeholder', 'code-drawing', 'partial', 'missing', 'undefined'}
|
||||||
|
|
||||||
|
|
||||||
|
def require(ok, message):
|
||||||
|
if not ok:
|
||||||
|
raise ValueError(message)
|
||||||
|
|
||||||
|
|
||||||
|
def sha(raw):
|
||||||
|
return hashlib.sha256(raw).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def decode(raw):
|
||||||
|
def unique(pairs):
|
||||||
|
result = {}
|
||||||
|
for key, value in pairs:
|
||||||
|
require(key not in result, 'duplicate JSON key: ' + key)
|
||||||
|
result[key] = value
|
||||||
|
return result
|
||||||
|
return json.loads(raw, object_pairs_hook=unique,
|
||||||
|
parse_constant=lambda s: (_ for _ in ()).throw(ValueError(s)))
|
||||||
|
|
||||||
|
|
||||||
|
def encoded(value):
|
||||||
|
return json.dumps(value, ensure_ascii=False, sort_keys=True, allow_nan=False)
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=None)
|
||||||
|
def blob(root, commit, path):
|
||||||
|
return subprocess.check_output(['git', 'show', commit + ':' + path], cwd=root)
|
||||||
|
|
||||||
|
|
||||||
|
PACKAGE_MUTABLE = {'state', 'legacy_state', 'current_summary', 'updated_at_jst'}
|
||||||
|
UNIT_MUTABLE = {'state', 'current_execution', 'remaining_gaps', 'updated_at_jst',
|
||||||
|
'submitted_commit', 'current_summary', 'progress_summary', 'evidence',
|
||||||
|
'completed_at_jst', 'implementation_commit', 'integrated_commit',
|
||||||
|
'integration_commit', 'normal_integration_candidate'}
|
||||||
|
|
||||||
|
|
||||||
|
def projection(packages):
|
||||||
|
require(type(packages) is list, 'package register type')
|
||||||
|
result = []
|
||||||
|
for package in packages:
|
||||||
|
require(type(package) is dict, 'package type')
|
||||||
|
row = {k: v for k, v in package.items() if k not in PACKAGE_MUTABLE}
|
||||||
|
require(type(row.get('remaining_deliverables')) is list, 'deliverables type')
|
||||||
|
row['remaining_deliverables'] = [{k: v for k, v in unit.items()
|
||||||
|
if k not in UNIT_MUTABLE} for unit in row['remaining_deliverables']]
|
||||||
|
result.append(row)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
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 symlink')
|
||||||
|
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()
|
||||||
|
spec_raw = raw(OUT + 'TASK_SPEC.json')
|
||||||
|
require(sha(spec_raw) == '8026040345de6b7d06cfce40beac9150bfe9ad303d6b26c11bd94e66b1080b29', 'raw spec SHA')
|
||||||
|
require(sha(spec_raw.decode().strip().encode()) == SPEC_HASH, 'immutable spec')
|
||||||
|
require(spec_raw == blob(root, BASE, OUT + 'TASK_SPEC.json'), 'raw immutable spec')
|
||||||
|
spec = decode(spec_raw)
|
||||||
|
require(spec['audit_source_anchor'] == ANCHOR, 'audit anchor')
|
||||||
|
require(sha(raw(OUT + 'FROZEN_GAME_INPUT_PINS.json')) == PINS_HASH ==
|
||||||
|
spec['frozen_game_input_pins_sha256'], 'frozen pins')
|
||||||
|
require(raw(OUT + 'ACTUAL_OPUS_NEXT_ORIGINAL_UNIT_SELECTION.json') ==
|
||||||
|
blob(root, BASE, OUT + 'ACTUAL_OPUS_NEXT_ORIGINAL_UNIT_SELECTION.json'), 'selection')
|
||||||
|
require(sha(raw(BASELINE)) == BASE_HASH, 'original source')
|
||||||
|
for name in ('work_packages.json', 'atomic_requirements.json', 'decisions.json'):
|
||||||
|
current, original = raw(PLAN + name), blob(root, BASE, PLAN + name)
|
||||||
|
if name == 'work_packages.json':
|
||||||
|
require(encoded(projection(decode(current))) == encoded(projection(decode(original))),
|
||||||
|
'original definitions')
|
||||||
|
else:
|
||||||
|
require(current == original, 'original register: ' + name)
|
||||||
|
pins = decode(raw(OUT + 'FROZEN_GAME_INPUT_PINS.json'))
|
||||||
|
actual = selection(root)
|
||||||
|
require(encoded(actual) == encoded(pins['source_files']), 'selected game set: missing/new/changed')
|
||||||
|
require(len(actual) == 1368, 'selected game cardinality')
|
||||||
|
for key in ('marker_contract', 'policy', 'engine'):
|
||||||
|
path = Path(pins[key + '_path']) if key == 'engine' else root / pins[key + '_path']
|
||||||
|
require(sha(path.read_bytes()) == pins[key + '_sha256'], key + ' identity')
|
||||||
|
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')
|
||||||
|
require(sum(p.endswith('.gd') for p in actual) == pins['parse_files'] == 329, 'parse set')
|
||||||
|
# Every selected file, including settings and default player identity, is bound
|
||||||
|
# to both the immutable source map and the exact published game audit anchor.
|
||||||
|
for path in actual:
|
||||||
|
require(sha(blob(root, ANCHOR, path)) == actual[path]['sha256'], 'audit source: ' + path)
|
||||||
|
return {'selected_files': len(actual), 'parse_files': 329,
|
||||||
|
'source_anchor': ANCHOR, 'native_reuse_accepted': False,
|
||||||
|
'native_reuse_reason': 'Exact game equality is checked; external native proof and exact S/C applicability remain independent review gates.'}
|
||||||
|
|
||||||
|
|
||||||
|
def read_source(root, path):
|
||||||
|
if path.startswith(P22):
|
||||||
|
archived = blob(root, PUBLISHED, ARCHIVE + path[len(P22):])
|
||||||
|
if (root / path).exists():
|
||||||
|
require((root / path).read_bytes() == archived, 'P22 alias changed: ' + path)
|
||||||
|
return archived
|
||||||
|
return (root / path).read_bytes()
|
||||||
|
|
||||||
|
|
||||||
|
def source(root, path):
|
||||||
|
raw = read_source(root, path)
|
||||||
|
anchor = PUBLISHED if path.startswith(P22) else ANCHOR
|
||||||
|
archived_path = ARCHIVE + path[len(P22):] if path.startswith(P22) else path
|
||||||
|
anchored = blob(root, anchor, archived_path)
|
||||||
|
require(raw == anchored, 'stale evidence: ' + path)
|
||||||
|
return {'path': path, 'sha256': sha(raw), 'bytes': len(raw),
|
||||||
|
'anchor': anchor, 'anchor_path': archived_path}
|
||||||
|
|
||||||
|
|
||||||
|
def excerpt(root, path, start, end):
|
||||||
|
pin = source(root, path)
|
||||||
|
return dict(pin, lines=[start, end], text='\n'.join(
|
||||||
|
read_source(root, path).decode().splitlines()[start-1:end]))
|
||||||
|
|
||||||
|
|
||||||
|
def expected(root):
|
||||||
|
"""Independent original clauses plus pinned actual evidence, not ledger-derived facts."""
|
||||||
|
lines = (root / BASELINE).read_text().splitlines()
|
||||||
|
require(sha((root / BASELINE).read_bytes()) == BASE_HASH, 'baseline')
|
||||||
|
manifest_raw = blob(root, PUBLISHED, ARCHIVE + 'ROOT_MATERIAL_PRESERVATION_MANIFEST.json')
|
||||||
|
require(sha(manifest_raw) == MANIFEST_HASH, 'published P22 manifest')
|
||||||
|
for entry in decode(manifest_raw)['files']:
|
||||||
|
raw = read_source(root, entry['original_path'])
|
||||||
|
require(len(raw) == entry['bytes'] and sha(raw) == entry['sha256'], 'published P22 file pin')
|
||||||
|
def cite(start, end=None):
|
||||||
|
end = end or start
|
||||||
|
return {'path': BASELINE, 'lines': [start, end],
|
||||||
|
'text': '\n'.join(lines[start-1:end]), 'sha256': BASE_HASH}
|
||||||
|
rows = []
|
||||||
|
def add(identity, kind, spans, condition, owner, status='undefined', evidence=None,
|
||||||
|
reason='欠損: 個別ID・必要量・完成内容は原本未定義。D08/D09未決。', identity_data=None):
|
||||||
|
rows.append({'id': identity, 'kind': kind, 'identity': identity_data,
|
||||||
|
'citations': [cite(*span) for span in spans], 'required_condition': condition,
|
||||||
|
'status': status, 'required_count': None, 'completed_count': None,
|
||||||
|
'evidence': evidence or [], 'missing': 'original production content missing or undefined',
|
||||||
|
'reason': reason, 'owner': owner})
|
||||||
|
add('chapters.all', 'chapter', [(8,), (26,), (27,30), (256,)],
|
||||||
|
'メインストーリー全章。原本の仕様書章番号をシナリオ章と混同しない。章名・章数・順序・場面一覧は未定義。', 'D08')
|
||||||
|
add('endings.all', 'ending', [(9,), (135,)],
|
||||||
|
'全エンドを要求。個別エンドID・本数・到達条件は未定義。', 'D08')
|
||||||
|
add('endings.bad', 'ending', [(135,)],
|
||||||
|
'特定バッドエンド確定時の敵/背景オブジェクト化。カテゴリのみ原本定義、対象人物/場面/数は未定義。', 'P26-02')
|
||||||
|
add('endings.game_over', 'ending', [(72,)],
|
||||||
|
'赤字決算3回連続のゲームオーバー。物語エンド数への換算不可。', 'P26-02', 'missing',
|
||||||
|
[source(root, 'scripts/economy/economy_service.gd')],
|
||||||
|
'欠損: 経済処理の既存コードは存在するが終点のシナリオ・画像・音声・全受入を証明しない。')
|
||||||
|
add('encyclopedia.all', 'encyclopedia', [(9,)],
|
||||||
|
'図鑑コンプ。対象集合・項目ID・解放条件・必要項目数は未定義。', 'D08')
|
||||||
|
add('dialogue.all', 'dialogue', [(155,), (230,), (256,)],
|
||||||
|
'会話/イベント用台詞とシナリオ。本文・話者・行数・場面単位は未定義。UI通知文を完成脚本に数えない。', 'P26-02')
|
||||||
|
add('portraits.all', 'portrait', [(160,161), (230,), (235,), (242,)],
|
||||||
|
'メイン/サブ人物の表情・衣装・破損・汚れ・刻印・妊婦外見・ルート別描き分け。差分枚数未定義。', 'P26-03', 'missing')
|
||||||
|
add('voice.all', 'voice', [(252,), (257,)],
|
||||||
|
'キャラクターボイスの収録・実装。話者/台詞/クリップ数/担当未定義。BGMやUI音は代用不可。', 'P26-03', 'missing')
|
||||||
|
# All applicable full clauses are retained even where the original defines no finite scene list.
|
||||||
|
for key, spans, condition in [
|
||||||
|
('world', [(27,30)], '舞台・主人公・7人物の3運命を保つ。'),
|
||||||
|
('scope', [(31,34)], '原本表現要求と今回の非露骨な監査範囲との差異を残す。'),
|
||||||
|
('route_skills', [(132,), (134,135)], 'ルートによる技能・衣装補正・離脱条件。'),
|
||||||
|
('route_rules', [(138,161)], '3軸/盾・侵食・3種決闘・プレイヤー行動・履歴/外見の全共通条件。'),
|
||||||
|
('art', [(229,230), (234,235)], '原本のマップ/SD/立ち絵・モーションと描き分け。'),
|
||||||
|
('cg', [(242,243)], '人物立ち絵およびルート別CG/差分/モブ汎用スチル。'),
|
||||||
|
('motion', [(245,246)], '待機・歩行・作業・気絶・原本必須モーションと効果。'),
|
||||||
|
('production', [(252,), (255,258)], '原本のシナリオ/素材量産/音声制作と受入は未完。')]:
|
||||||
|
add('clause.' + key, 'category', spans, condition, 'P26-03', 'missing')
|
||||||
|
table = decode((root / P19).read_bytes())
|
||||||
|
table_pin = source(root, P19)
|
||||||
|
cells = {(c['character_id'], c['route_id']): c for c in table['cells']}
|
||||||
|
for i, (person, name) in enumerate(PEOPLE):
|
||||||
|
require(name in lines[117+i], 'original identity')
|
||||||
|
for route, route_name in ROUTES.items():
|
||||||
|
cell = cells[(person, route)]
|
||||||
|
require(cell['name'] == name and cell['route'] == route_name and
|
||||||
|
cell['natural_route_complete'] is False, 'P19 route identity/status')
|
||||||
|
add('route.' + person + '.' + route, 'route', [(118+i,), (30,), (132,), (138,161)],
|
||||||
|
'人物固有の入口・イベント列・分岐・終点・台詞・立ち絵・音声・履歴/保存と通常到達。',
|
||||||
|
'P26-02', 'missing', [dict(table_pin, cell=[person, route])],
|
||||||
|
'欠損: P19共通モデル/保存の部分実装は人物別制作/到達ではない。人物固有条件・場面列はD08未決。',
|
||||||
|
{'character_id': person, 'original_name': name, 'route_id': route, 'route_name': route_name})
|
||||||
|
# Preserve every existing usage record (614 usage groups, never 614 finished assets).
|
||||||
|
usage_inputs = ['facility_usage_slots.json', 'character_usage_slots.json',
|
||||||
|
'other_usage_slots.json', 'state_usage_groups.json']
|
||||||
|
usage_snapshots = {}
|
||||||
|
for filename in usage_inputs:
|
||||||
|
path = P22 + filename
|
||||||
|
records = decode(read_source(root, path))
|
||||||
|
usage_snapshots[path] = {'source': source(root, path), 'records': records}
|
||||||
|
for record in records:
|
||||||
|
anchors = record['source_anchors']
|
||||||
|
for a in anchors:
|
||||||
|
require(a['sha256'] == BASE_HASH and a['text'] == lines[a['line']-1], 'P22 citation')
|
||||||
|
representations = []
|
||||||
|
for rep in record.get('representations', []):
|
||||||
|
pin = source(root, rep['path'])
|
||||||
|
representations.append(dict(pin, historical_inventory=rep,
|
||||||
|
current_bytes_equal_inventory=pin['sha256'] == rep['sha256']))
|
||||||
|
kind = record.get('kind', 'usage')
|
||||||
|
status = 'missing'
|
||||||
|
if representations:
|
||||||
|
status = 'code-drawing' if all(r['historical_inventory']['representation'] == 'code_drawn'
|
||||||
|
for r in representations) else 'placeholder'
|
||||||
|
slot_id = record.get('slot_id', record.get('group_id'))
|
||||||
|
add('usage.' + slot_id, kind, [(a['line'],) for a in anchors],
|
||||||
|
record['usage_condition'], 'P26-03', status, representations,
|
||||||
|
'欠損: 利用枠/用途群。既存表現の現物SHAを再取得。汎用描画/仮素材は固有人物・必須差分の完成証明ではない。D09数量/品質/担当未決。',
|
||||||
|
{'usage_source': path, 'slot_id': slot_id})
|
||||||
|
references = {p: source(root, p) for p in [P19, BACKLOG] +
|
||||||
|
[P22 + n for n in ['contract.json', 'source_pins.json', 'current_media_inventory.json']]}
|
||||||
|
actual_support = [excerpt(root, 'data/characters/mvp_unique_characters.json', 1, 32),
|
||||||
|
excerpt(root, 'scripts/relationship/original_relationship_state.gd', 4, 34),
|
||||||
|
excerpt(root, 'autoload/simulation_runtime.gd', 6791, 6812),
|
||||||
|
excerpt(root, 'scripts/relationship/relationship_service.gd', 423, 479),
|
||||||
|
excerpt(root, 'scripts/ui/pixel_people.gd', 4, 18)]
|
||||||
|
media = decode(read_source(root, P22 + 'current_media_inventory.json'))
|
||||||
|
current_media = [dict(source(root, m['path']), historical_inventory=m) for m in media]
|
||||||
|
pins = decode((root / OUT / 'FROZEN_GAME_INPUT_PINS.json').read_bytes())
|
||||||
|
available_assets = {p: v for p, v in pins['source_files'].items() if p.startswith('assets/')}
|
||||||
|
return {'task_id': 'P26-01-LEDGER', 'schema_version': 1, 'source_anchor': ANCHOR,
|
||||||
|
'definition_base': BASE, 'rows': rows, 'usage_snapshots': usage_snapshots,
|
||||||
|
'published_material': {'anchor': PUBLISHED, 'manifest_sha256': MANIFEST_HASH,
|
||||||
|
'manifest_path': ARCHIVE + 'ROOT_MATERIAL_PRESERVATION_MANIFEST.json'},
|
||||||
|
'references': references, 'P22_03_authority': 'draft/unaccepted reference only',
|
||||||
|
'actual_support': actual_support,
|
||||||
|
'support_interpretation': 'Explicit state/identity/container support only; generic pose has no original character-specific appearance. No finished chapter/dialogue/portrait/voice/encyclopedia inferred.',
|
||||||
|
'current_media_reaudit': current_media,
|
||||||
|
'available_asset_source_set': available_assets,
|
||||||
|
'pending': {'D05': 'unresolved existing questions; existing approvals preserved',
|
||||||
|
'D08': 'unresolved', 'D09': 'unresolved'},
|
||||||
|
'accounting': {'packages': 30, 'units': 88, 'completed': 15, 'credit': 0,
|
||||||
|
'partial_credit': False, 'whole_unit_accepted': False},
|
||||||
|
'native_evidence': {'reuse_accepted': False, 'new_full_run': False,
|
||||||
|
'historical_only': {'suites': 184, 'checks': 21903, 'parse': 329, 'owned_ended': 186},
|
||||||
|
'reason': 'Runner must independently establish exact S/C applicability; no native result claimed here.'}}
|
||||||
|
|
||||||
|
|
||||||
|
def verify(candidate, canonical):
|
||||||
|
require(type(candidate) is dict, 'closed document object')
|
||||||
|
require(type(candidate.get('rows')) is list, 'closed rows list')
|
||||||
|
rows = candidate['rows']
|
||||||
|
ids = [r['id'] for r in rows]
|
||||||
|
require(len(ids) == len(set(ids)), 'duplicate row ID')
|
||||||
|
require(set(ids) == {r['id'] for r in canonical['rows']}, 'original clause/slot coverage')
|
||||||
|
for row in rows:
|
||||||
|
require(row['status'] in STATUSES, 'invalid status')
|
||||||
|
require(row['required_count'] is None and row['completed_count'] is None, 'invented quantity')
|
||||||
|
require(row['status'] != 'produced', 'unsupported completion')
|
||||||
|
require(row['owner'] in {'D08', 'P26-02', 'P26-03'}, 'invalid owner')
|
||||||
|
require(row['citations'] and row['reason'] and row['missing'], 'missing traceability')
|
||||||
|
require(encoded(candidate) == encoded(canonical), 'ledger differs from original/source-bound audit')
|
||||||
|
|
||||||
|
|
||||||
|
def markdown(document):
|
||||||
|
# A lossless deterministic human view, including exact citations and source hashes.
|
||||||
|
lines = ['# P26-01 全必要内容の台帳', '',
|
||||||
|
'15/88、今回加算0。未制作/未定義は欠損。利用枠数は完成数ではない。', '',
|
||||||
|
'D05既存未解決事項・D08・D09は未解決。P22-03は未受入の参考資料。', '']
|
||||||
|
for row in document['rows']:
|
||||||
|
lines += ['## ' + row['id'], '', '- 種別: ' + row['kind'],
|
||||||
|
'- 識別: ' + encoded(row['identity']),
|
||||||
|
'- 状態: ' + row['status'], '- 所管: ' + row['owner'],
|
||||||
|
'- 条件: ' + (row['required_condition'] if isinstance(row['required_condition'], str)
|
||||||
|
else encoded(row['required_condition'])), '- 理由: ' + row['reason'],
|
||||||
|
'- 必要数/完成数: 未定義 / 未計数', '- 欠損: ' + row['missing'], '']
|
||||||
|
for c in row['citations']:
|
||||||
|
lines += [f"原本 `{c['path']}:{c['lines'][0]}-{c['lines'][1]}` SHA256 `{c['sha256']}`", '',
|
||||||
|
'```text', c['text'], '```', '']
|
||||||
|
lines += ['証拠(空配列は明示的欠損):', '', '```json',
|
||||||
|
json.dumps(row['evidence'], ensure_ascii=False, indent=2), '```', '']
|
||||||
|
lines += ['## 固定参照・監査境界', '', '```json',
|
||||||
|
json.dumps({k: v for k, v in document.items() if k != 'rows'}, ensure_ascii=False, indent=2), '```', '']
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def check_view(raw, document):
|
||||||
|
require(raw == markdown(document).encode(), 'Markdown drift')
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument('--project-root', type=Path, default=Path('.'))
|
||||||
|
mode = parser.add_mutually_exclusive_group(required=True)
|
||||||
|
mode.add_argument('--check', action='store_true')
|
||||||
|
mode.add_argument('--source-binding-only', action='store_true')
|
||||||
|
mode.add_argument('--write', action='store_true')
|
||||||
|
args = parser.parse_args()
|
||||||
|
root = args.project_root.resolve()
|
||||||
|
try:
|
||||||
|
result = binding(root)
|
||||||
|
canonical = expected(root)
|
||||||
|
if args.write:
|
||||||
|
(root / OUT / 'CONTENT_LEDGER.json').write_text(json.dumps(canonical, ensure_ascii=False, indent=2) + '\n')
|
||||||
|
(root / OUT / 'CONTENT_LEDGER.md').write_text(markdown(canonical))
|
||||||
|
else:
|
||||||
|
document = decode((root / OUT / 'CONTENT_LEDGER.json').read_bytes())
|
||||||
|
verify(document, canonical)
|
||||||
|
if args.check:
|
||||||
|
check_view((root / OUT / 'CONTENT_LEDGER.md').read_bytes(), document)
|
||||||
|
print(json.dumps({'documentation_check': 'pass', **result}, ensure_ascii=False))
|
||||||
|
return 0
|
||||||
|
except (ValueError, KeyError, TypeError, OSError, subprocess.CalledProcessError) as error:
|
||||||
|
print('P26 audit rejected: ' + str(error), file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Runner negative controls for P26; no fixture writes and no native execution."""
|
||||||
|
import argparse
|
||||||
|
import copy
|
||||||
|
from pathlib import Path
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
import check_content_ledger as audit
|
||||||
|
|
||||||
|
|
||||||
|
class LedgerTests(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.canonical = audit.expected(ROOT)
|
||||||
|
cls.submitted = audit.decode((ROOT / audit.OUT / 'CONTENT_LEDGER.json').read_bytes())
|
||||||
|
|
||||||
|
def reject(self, mutate):
|
||||||
|
value = copy.deepcopy(self.submitted)
|
||||||
|
mutate(value)
|
||||||
|
with self.assertRaises((ValueError, TypeError, KeyError)):
|
||||||
|
audit.verify(value, self.canonical)
|
||||||
|
|
||||||
|
def test_valid_submitted_and_independent_source_control(self):
|
||||||
|
audit.verify(self.submitted, self.canonical)
|
||||||
|
audit.verify(copy.deepcopy(self.canonical), self.canonical)
|
||||||
|
audit.check_view((ROOT / audit.OUT / 'CONTENT_LEDGER.md').read_bytes(), self.submitted)
|
||||||
|
|
||||||
|
def test_required_sets_are_independent_of_submitted_rows(self):
|
||||||
|
ids = {r['id'] for r in self.canonical['rows']}
|
||||||
|
self.assertTrue({'chapters.all', 'endings.all', 'endings.bad', 'endings.game_over',
|
||||||
|
'dialogue.all', 'portraits.all', 'voice.all', 'encyclopedia.all'} <= ids)
|
||||||
|
self.assertEqual({r['id'] for r in self.canonical['rows'] if r['kind'] == 'route'},
|
||||||
|
{'route.' + p + '.' + r for p, _ in audit.PEOPLE for r in audit.ROUTES})
|
||||||
|
|
||||||
|
def test_omitted_original_categories_and_route(self):
|
||||||
|
for identity in ['chapters.all', 'route.satsuki.corruption', 'endings.all',
|
||||||
|
'endings.bad', 'endings.game_over', 'dialogue.all', 'voice.all',
|
||||||
|
'portraits.all', 'encyclopedia.all', 'clause.route_rules']:
|
||||||
|
with self.subTest(identity=identity):
|
||||||
|
self.reject(lambda d: d.update(rows=[r for r in d['rows'] if r['id'] != identity]))
|
||||||
|
|
||||||
|
def test_missing_usage_slot(self):
|
||||||
|
self.reject(lambda d: d['rows'].pop())
|
||||||
|
|
||||||
|
def test_fabricated_quantity_and_false_completion(self):
|
||||||
|
self.reject(lambda d: d['rows'][0].update(required_count=8))
|
||||||
|
self.reject(lambda d: d['rows'][0].update(completed_count=0))
|
||||||
|
self.reject(lambda d: d['rows'][0].update(status='produced', missing=None))
|
||||||
|
|
||||||
|
def test_closed_schema_owner_status_type_and_duplicates(self):
|
||||||
|
for field, value in [('owner', 'P99-99'), ('status', 'pass'), ('kind', 'invented'),
|
||||||
|
('required_count', False), ('citations', []), ('extra', True)]:
|
||||||
|
with self.subTest(field=field):
|
||||||
|
self.reject(lambda d: d['rows'][0].update({field: value}))
|
||||||
|
self.reject(lambda d: d['rows'].append(copy.deepcopy(d['rows'][0])))
|
||||||
|
self.reject(lambda d: d.update(schema_version=True))
|
||||||
|
|
||||||
|
def test_exact_route_identity(self):
|
||||||
|
self.reject(lambda d: next(r for r in d['rows'] if r['kind'] == 'route')
|
||||||
|
['identity'].update(original_name='別人'))
|
||||||
|
|
||||||
|
def test_stale_original_and_source(self):
|
||||||
|
self.reject(lambda d: d['rows'][0]['citations'][0].update(text='stale'))
|
||||||
|
self.reject(lambda d: d['rows'][0]['citations'][0].update(sha256='0' * 64))
|
||||||
|
self.reject(lambda d: d['references'][audit.P19].update(sha256='0' * 64))
|
||||||
|
self.reject(lambda d: d.update(source_anchor='HEAD'))
|
||||||
|
|
||||||
|
def test_malformed_json_duplicate_keys_and_nonfinite(self):
|
||||||
|
self.assertEqual(audit.decode('{"rows": []}'), {'rows': []})
|
||||||
|
for raw in ['{', '{"x":1,"x":2}', '{"x":NaN}', '{"x":Infinity}']:
|
||||||
|
with self.subTest(raw=raw), self.assertRaises(ValueError):
|
||||||
|
audit.decode(raw)
|
||||||
|
|
||||||
|
def test_markdown_drift(self):
|
||||||
|
view = audit.markdown(self.submitted).encode()
|
||||||
|
audit.check_view(view, self.submitted)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
audit.check_view(view + b'\n', self.submitted)
|
||||||
|
|
||||||
|
def test_definition_projection_allows_only_progress(self):
|
||||||
|
packages = audit.decode(audit.blob(ROOT, audit.BASE, audit.PLAN + 'work_packages.json'))
|
||||||
|
changed = copy.deepcopy(packages)
|
||||||
|
changed[0]['state'] = 'progress-only-control'
|
||||||
|
changed[0]['remaining_deliverables'][0]['evidence'] = ['progress-only-control']
|
||||||
|
self.assertEqual(audit.encoded(audit.projection(packages)), audit.encoded(audit.projection(changed)))
|
||||||
|
for field in ['acceptance', 'work', 'depends_on', 'requirement_ids']:
|
||||||
|
changed = copy.deepcopy(packages)
|
||||||
|
changed[0][field] = 'mutated definition'
|
||||||
|
self.assertNotEqual(audit.encoded(audit.projection(packages)), audit.encoded(audit.projection(changed)))
|
||||||
|
|
||||||
|
def test_published_source_changed_bytes_rejected(self):
|
||||||
|
original = audit.blob
|
||||||
|
def stale(root, anchor, path):
|
||||||
|
raw = original(root, anchor, path)
|
||||||
|
return raw + b' ' if path == audit.ARCHIVE + 'ROOT_MATERIAL_PRESERVATION_MANIFEST.json' else raw
|
||||||
|
with patch.object(audit, 'blob', stale), self.assertRaises(ValueError):
|
||||||
|
audit.expected(ROOT)
|
||||||
|
|
||||||
|
def test_selected_input_mutation_rejected(self):
|
||||||
|
actual = audit.selection(ROOT)
|
||||||
|
# Valid map control comes from frozen independent pins, not a second selection call.
|
||||||
|
pins = audit.decode((ROOT / audit.OUT / 'FROZEN_GAME_INPUT_PINS.json').read_bytes())
|
||||||
|
self.assertEqual(audit.encoded(actual), audit.encoded(pins['source_files']))
|
||||||
|
for mode in ('missing', 'new', 'changed', 'wrong_type'):
|
||||||
|
changed = copy.deepcopy(actual)
|
||||||
|
key = 'project.godot'
|
||||||
|
if mode == 'missing':
|
||||||
|
del changed[key]
|
||||||
|
elif mode == 'new':
|
||||||
|
changed['data/unapproved.json'] = {'bytes': 0, 'sha256': '0' * 64}
|
||||||
|
elif mode == 'changed':
|
||||||
|
changed[key]['sha256'] = '0' * 64
|
||||||
|
else:
|
||||||
|
changed[key]['bytes'] = str(changed[key]['bytes'])
|
||||||
|
with self.subTest(mode=mode), patch.object(audit, 'selection', return_value=changed), self.assertRaises(ValueError):
|
||||||
|
audit.binding(ROOT)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('--project-root', type=Path, default=Path('.'))
|
||||||
|
args, remaining = parser.parse_known_args()
|
||||||
|
ROOT = args.project_root.resolve()
|
||||||
|
unittest.main(argv=[__file__] + remaining)
|
||||||
Reference in New Issue
Block a user