sustain · Stage 4

未知を残したままlegacy systemを安全に変更する

小さなlegacy fixtureから実行経路、変更理由、未知領域を地図化し、振る舞いを保存するcharacterization testを変更前の証拠として構築する。

学習時間
300分
難易度
advanced
更新日
2026-07-30
到達証拠
成果物・説明・判断根拠・転用

到達目標

  1. change requestから実際に通るexecution pathと未知領域を編集前に特定できる

    • 実行経路、変更理由、未知領域を示すシステム地図と特性テスト
    • program comprehensionとmaintenance processを変更前の証拠へ結ぶ5分発表
  2. 正しさを断定せず観測済みのlegacy behaviorをcharacterization testへ固定できる

    • 実行経路、変更理由、未知領域を示すシステム地図と特性テスト
    • 局所的に見える変更の隠れた経路とテスト不足を診断する回答
  3. change requestだけが変わる条件で影響経路と必要な特性テストを再構成できる

    • 局所的に見える変更の隠れた経路とテスト不足を診断する回答
    • 税丸め変更へ要求だけを変えて地図と特性テストを再構成した記録

能力の進行

  1. recognize

    change request、実行経路、未知領域、観測済み振る舞いを区別できる

    証拠: 実行経路、変更理由、未知領域を示すシステム地図と特性テスト

  2. explain

    編集前のprogram comprehensionとcharacterization testが必要な理由を説明できる

    証拠: program comprehensionとmaintenance processを変更前の証拠へ結ぶ5分発表

  3. apply

    小さなfixtureを追跡し実行経路と特性テストを再現可能に残せる

    証拠: 実行経路、変更理由、未知領域を示すシステム地図と特性テスト

  4. diagnose

    未観測の依存、暗黙の副作用、誤った影響範囲の断定を反証できる

    証拠: 局所的に見える変更の隠れた経路とテスト不足を診断する回答

  5. lead

    未知を隠さず停止条件と追加調査のownerを含む変更計画を主導できる

    証拠: 税丸め変更へ要求だけを変えて地図と特性テストを再構成した記録

なぜ重要か

legacy systemの変更で最初に必要なのは編集ではなく、要求がどのentry pointからどの処理とデータを通るかを説明できる証拠である。名前検索だけでは動的な分岐、暗黙の共有状態、運用手順、誰もownerを説明できないunknownを落とす。program comprehensionは、この未知を段階的に減らしながら変更仮説を検査する活動である。

SWEBOK Guide Version 4.0aはsoftware maintenanceを知識領域として整理し、ISO/IEC/IEEE 14764:2022はlife cycleにおけるmaintenance processを扱う。2000年のmaintenance roadmapと2007年のprogram comprehension研究は、変更がコード編集だけでなく理解、進化、組織知識の問題でもあることを示す。ここでは標準の章名を暗記せず、change request、system map、characterization test、停止判断へ接続する。

メンタルモデル

変更を「request、execution path、observed behavior、unknown、decision」の連鎖として扱う。先にchange reasonを固定し、小さいfixtureでentry pointから経路を追う。owner、仕様根拠、観測が欠ける場所はunknownとして消さず、追加調査と停止条件へ結ぶ。最後に現在の出力をcharacterization testへ保存し、編集後の差分を意図した変化と回帰へ分ける。

system mapは全systemを描く百科事典ではない。対象change requestに必要な範囲と、範囲外だと判断した根拠を示す作業仮説である。要求が変われば、同じfixtureでもaffected pathと必要なcharacterization testを再計算する。

change requestから変更前証拠を作るlegacy comprehension loop

注記

図を読む際の補足情報です。

  1. この注記は旧図の読み順を保持する補助です。
  2. Reason: 誰のどの結果を変える要求かを一文で固定する。
  3. Trace: entry pointから実際のexecution pathを入力付きで追う。
  4. Map: component、data、side effect、ownerを経路へ結ぶ。
  5. Unknown: 未観測、未所有、仕様不明を仮説から分離する。
  6. Characterize: 現在のexpected、actual、observed pathをtestへ残す。
  7. Decide: 証拠が足りなければ編集せず調査または停止を選ぶ。

編集前に、要求からexecution pathと未知領域をどこまで証拠化できているか。

変更前証拠
編集せずに要求と現行挙動を結ぶcomprehension範囲。
  • Reason
    誰のどの結果を変える要求かを一文で固定する。
    component
    変更前証拠
  • Trace
    entry pointから実際のexecution pathを入力付きで追う。
    component
    変更前証拠
  • Map
    component、data、side effect、ownerを経路へ結ぶ。
    component
    変更前証拠
  • Unknown
    未観測、未所有、仕様不明を仮説から分離する。
    component
    変更前証拠
  • Characterize
    現在のexpected、actual、observed pathをtestへ残す。
    component
    変更前証拠
  • Decide
    証拠が足りなければ編集せず調査または停止を選ぶ。
    component
    変更前証拠
  • Reason → Trace: 変更対象の実行経路を選ぶ
  • Trace → Map: 観測したcomponentと副作用を結ぶ
  • Map → Unknown: 未観測と仮説を分離する
  • Unknown → Characterize: 現行挙動をtestへ固定する
  • Characterize → Decide: 証拠量で編集開始を判断する

change requestから実行経路、component、side effect、未知領域、characterization test、停止判断までを指し示せる。

動く例で考える

小さなlegacy請求fixtureを編集前に地図化する

前提
実サービスではないlesson-defined synthetic fixtureを使う。production traffic、実顧客、実価格を観測したデータではなく、program comprehension手順の因果を検査するための小さな例である。
入力
割引率変更と税丸め変更の二つのchange request、四つの処理stage、既存入出力case、ownerと観測状態を固定する。baselineでは割引率変更だけを選ぶ。
操作
選んだrequestを扱うstageをexecution pathとして入力から導き、ownerまたは観測が欠けるstageをunknownへ残す。そのpathを使ったcaseだけをcharacterization testへ変換する。
観測
baselineとtransferではlegacy fixtureは同じでもaffected pathとcharacterization testが異なる。経路計算を空にするとmaintenance-comprehension-invariantで停止する。
結論
変更開始の条件はcommand successではなく、change reason、affected path、unknown、観測済み出力が同じ証拠へ結ばれたsystem outcomeである。
python3.13 - <<'PY'
import json

HARNESS = "legacy_comprehension_lab_v1"
TRANSFER_TASK = (
    "割引率変更から税丸め変更へchange requestだけを変え、"
    "同じlegacy fixtureの影響経路、未知領域、特性テストを再構成する"
)
FIXTURE = {
    "fixture_id": "legacy-invoice-v1",
    "entry_point": "receive-invoice",
    "stages": [
        {
            "id": "receive-invoice",
            "change_requests": ["discount-rate-change", "tax-rounding-change"],
            "owner": "billing-intake",
            "observed": True,
        },
        {
            "id": "lookup-discount",
            "change_requests": ["discount-rate-change"],
            "owner": "",
            "observed": True,
        },
        {
            "id": "calculate-subtotal",
            "change_requests": ["discount-rate-change", "tax-rounding-change"],
            "owner": "billing-core",
            "observed": True,
        },
        {
            "id": "round-tax",
            "change_requests": ["tax-rounding-change"],
            "owner": "billing-core",
            "observed": False,
        },
        {
            "id": "persist-invoice",
            "change_requests": ["discount-rate-change", "tax-rounding-change"],
            "owner": "billing-storage",
            "observed": True,
        },
    ],
    "cases": [
        {
            "id": "discount-standard",
            "change_request": "discount-rate-change",
            "input_cents": 10000,
            "discount_basis_points": 1000,
            "tax_basis_points": 0,
            "tax_rounding_increment": 1,
            "observed_output_cents": 9000,
        },
        {
            "id": "discount-boundary",
            "change_request": "discount-rate-change",
            "input_cents": 10001,
            "discount_basis_points": 500,
            "tax_basis_points": 0,
            "tax_rounding_increment": 1,
            "observed_output_cents": 9500,
        },
        {
            "id": "tax-rounding-standard",
            "change_request": "tax-rounding-change",
            "input_cents": 10001,
            "discount_basis_points": 0,
            "tax_basis_points": 1000,
            "tax_rounding_increment": 5,
            "observed_output_cents": 11001,
        },
        {
            "id": "tax-rounding-boundary",
            "change_request": "tax-rounding-change",
            "input_cents": 9999,
            "discount_basis_points": 0,
            "tax_basis_points": 800,
            "tax_rounding_increment": 10,
            "observed_output_cents": 10799,
        },
    ],
}
BASELINE_CHANGE = {
    "change_request": "discount-rate-change",
}
TRANSFER_CHANGE = {
    "change_request": "tax-rounding-change",
}
FIXTURE_FIELDS = {"fixture_id", "entry_point", "stages", "cases"}
STAGE_FIELDS = {"id", "change_requests", "owner", "observed"}
CASE_FIELDS = {
    "id",
    "change_request",
    "input_cents",
    "discount_basis_points",
    "tax_basis_points",
    "tax_rounding_increment",
    "observed_output_cents",
}
CHANGE_FIELDS = {"change_request"}

def validate_legacy_inputs(fixture, baseline_change, transfer_change):
    if (
        type(fixture) is not dict
        or set(fixture) != FIXTURE_FIELDS
        or type(fixture["fixture_id"]) is not str
        or not fixture["fixture_id"]
        or type(fixture["entry_point"]) is not str
        or not fixture["entry_point"]
        or type(fixture["stages"]) is not list
        or not fixture["stages"]
        or type(fixture["cases"]) is not list
        or not fixture["cases"]
    ):
        raise AssertionError(
            "maintenance-comprehension-invariant: invalid fixture schema"
        )
    stage_ids = []
    for stage in fixture["stages"]:
        if (
            type(stage) is not dict
            or set(stage) != STAGE_FIELDS
            or type(stage["id"]) is not str
            or not stage["id"]
            or type(stage["change_requests"]) is not list
            or not stage["change_requests"]
            or not all(
                type(request) is str and request
                for request in stage["change_requests"]
            )
            or type(stage["owner"]) is not str
            or type(stage["observed"]) is not bool
        ):
            raise AssertionError(
                "maintenance-comprehension-invariant: "
                "invalid stage schema"
            )
        stage_ids.append(stage["id"])
    if len(stage_ids) != len(set(stage_ids)):
        raise AssertionError(
            "maintenance-comprehension-invariant: duplicate stage id"
        )
    if fixture["entry_point"] not in stage_ids:
        raise AssertionError(
            "maintenance-comprehension-invariant: unknown entry point"
        )

    case_ids = []
    for case in fixture["cases"]:
        numeric_fields = (
            "input_cents",
            "discount_basis_points",
            "tax_basis_points",
            "tax_rounding_increment",
            "observed_output_cents",
        )
        if (
            type(case) is not dict
            or set(case) != CASE_FIELDS
            or type(case["id"]) is not str
            or not case["id"]
            or type(case["change_request"]) is not str
            or not case["change_request"]
            or not all(type(case[field]) is int for field in numeric_fields)
            or case["input_cents"] < 0
            or case["observed_output_cents"] < 0
            or not 0 <= case["discount_basis_points"] <= 10000
            or not 0 <= case["tax_basis_points"] <= 10000
            or case["tax_rounding_increment"] <= 0
        ):
            raise AssertionError(
                "maintenance-comprehension-invariant: "
                "invalid case schema"
            )
        case_ids.append(case["id"])
    if len(case_ids) != len(set(case_ids)):
        raise AssertionError(
            "maintenance-comprehension-invariant: duplicate case id"
        )

    changes = (baseline_change, transfer_change)
    if not all(
        type(change) is dict
        and set(change) == CHANGE_FIELDS
        and type(change["change_request"]) is str
        and change["change_request"]
        for change in changes
    ):
        raise AssertionError(
            "maintenance-comprehension-invariant: invalid change schema"
        )
    changed_fields = [
        field
        for field in sorted(CHANGE_FIELDS)
        if baseline_change[field] != transfer_change[field]
    ]
    known_requests = {
        request
        for stage in fixture["stages"]
        for request in stage["change_requests"]
    }
    case_requests = {
        case["change_request"]
        for case in fixture["cases"]
    }
    selected_requests = {
        baseline_change["change_request"],
        transfer_change["change_request"],
    }
    if (
        changed_fields != ["change_request"]
        or not selected_requests <= known_requests
        or not selected_requests <= case_requests
    ):
        raise AssertionError(
            "maintenance-comprehension-invariant: "
            "transfer changed wrong input"
        )
    return changed_fields

def fixed_legacy_fixture():
    # JSON値だけで構成したfixtureを別objectへ再 materializeし、
    # fixture_idだけの比較で構造driftを見逃さないようにする。
    return json.loads(json.dumps(FIXTURE, ensure_ascii=False))

def trace_execution(fixture, change_request):
    # change requestから経路を再計算し、ファイル名の推測を影響範囲の
    # 根拠にしない。
    return [
        stage["id"]
        for stage in fixture["stages"]
        if change_request in stage["change_requests"]
    ]

def execute_case(case):
    discounted = (
        case["input_cents"]
        * (10000 - case["discount_basis_points"])
        // 10000
    )
    tax = discounted * case["tax_basis_points"] // 10000
    increment = case["tax_rounding_increment"]
    rounded_tax = ((tax + increment - 1) // increment) * increment
    return discounted + rounded_tax

def characterize(fixture, change_request, affected_path):
    tests = []
    for case in fixture["cases"]:
        if case["change_request"] != change_request:
            continue
        actual = execute_case(case)
        # expectedは変更前に保存した独立観測値を読む。現実装を二度
        # 実行してexpectedを作ると同じbehavior driftを見逃すためである。
        expected = case["observed_output_cents"]
        tests.append(
            {
                "case_id": case["id"],
                "expected": expected,
                "actual": actual,
                "passed": actual == expected,
                "observed_path": list(affected_path),
            }
        )
    return tests

def analyze_change(fixture, change_request):
    affected_path = trace_execution(fixture, change_request)
    if not affected_path:
        raise AssertionError(
            "maintenance-comprehension-invariant: empty affected path"
        )
    stage_by_id = {
        stage["id"]: stage
        for stage in fixture["stages"]
    }
    unknowns = [
        {
            "stage": stage_id,
            "reason": (
                "owner-unknown"
                if not stage_by_id[stage_id]["owner"]
                else "behavior-not-observed"
            ),
        }
        for stage_id in affected_path
        if (
            not stage_by_id[stage_id]["owner"]
            or not stage_by_id[stage_id]["observed"]
        )
    ]
    tests = characterize(fixture, change_request, affected_path)
    if (
        len(tests) < 2
        or not all(test["passed"] and test["observed_path"] for test in tests)
    ):
        raise AssertionError(
            "maintenance-comprehension-invariant: evidence incomplete"
        )
    return {
        "fixture_id": fixture["fixture_id"],
        "change_request": change_request,
        "affected_path": affected_path,
        "unknowns": unknowns,
        "characterization_tests": tests,
    }

def main():
    changed_fields = validate_legacy_inputs(
        FIXTURE,
        BASELINE_CHANGE,
        TRANSFER_CHANGE,
    )
    baseline_fixture = fixed_legacy_fixture()
    transferred_fixture = fixed_legacy_fixture()
    if (
        baseline_fixture is transferred_fixture
        or baseline_fixture != transferred_fixture
    ):
        raise AssertionError(
            "maintenance-comprehension-invariant: fixture snapshot drift"
        )
    baseline = analyze_change(
        baseline_fixture,
        BASELINE_CHANGE["change_request"],
    )
    transferred = analyze_change(
        transferred_fixture,
        TRANSFER_CHANGE["change_request"],
    )
    if (
        baseline["affected_path"] == transferred["affected_path"]
        or baseline["characterization_tests"]
        == transferred["characterization_tests"]
    ):
        raise AssertionError(
            "maintenance-comprehension-invariant: transfer not causal"
        )
    report = {
        "harness": HARNESS,
        "fixture_metadata": {
            "kind": "synthetic",
            "provenance": "lesson-defined legacy-invoice-v1 input",
            "limitations": (
                "小さな決定的fixtureでありproductionの正しさ、"
                "全経路、業務仕様を表さない"
            ),
            "synthetic_or_observed_explicit": (
                "値はすべてsynthetic入力またはその観測計算結果"
            ),
        },
        "runtime_bound": {
            "records": (
                len(baseline_fixture["stages"])
                + len(baseline_fixture["cases"])
            ),
            "subprocesses": 0,
            "maximum_iterations": (
                len(baseline_fixture["stages"]) * 2
                + len(baseline_fixture["cases"]) * 2
            ),
        },
        "external_network_used": False,
        "system_map": {
            "entry_point": baseline_fixture["entry_point"],
            "execution_path": baseline["affected_path"],
            "change_reason": baseline["change_request"],
            "unknowns": baseline["unknowns"],
        },
        "change_analysis": {
            "change_request": baseline["change_request"],
            "affected_path": baseline["affected_path"],
        },
        "characterization_tests": baseline["characterization_tests"],
        "change_request_transfer": {
            "changed_assumption": "change-request",
            "changed_fields": changed_fields,
            "same_legacy_fixture": (
                baseline_fixture == transferred_fixture
            ),
            "baseline_fixture_snapshot": baseline_fixture,
            "transferred_fixture_snapshot": transferred_fixture,
            "baseline_affected_path": baseline["affected_path"],
            "transferred_affected_path": transferred["affected_path"],
            "baseline_characterization_tests": (
                baseline["characterization_tests"]
            ),
            "transferred_characterization_tests": (
                transferred["characterization_tests"]
            ),
        },
        "command_success_distinction": {
            "command_completed": True,
            "system_outcome_checked": bool(
                baseline["affected_path"]
                and baseline["characterization_tests"]
            ),
            "command_success_equals_system_outcome": False,
            "outcome_evidence": (
                "affected path、unknown、characterization testsを検査"
            ),
        },
        "mastery_evidence": {
            "lab_steps": [
                {"step": 1, "evidence": baseline["affected_path"]},
                {"step": 2, "evidence": baseline["characterization_tests"]},
                {"step": 3, "evidence": transferred["affected_path"]},
            ],
            "assessments": [
                {"assessment": 1, "evidence": baseline["unknowns"]},
                {
                    "assessment": 2,
                    "evidence": baseline["characterization_tests"],
                },
            ],
            "rubric_dimensions": [
                "technical-correctness",
                "judgment",
                "evidence",
                "communication",
            ],
            "transfer": {
                "task": TRANSFER_TASK,
                "changed_assumption": "change-request",
                "evidence": transferred["affected_path"],
            },
        },
    }
    print(json.dumps(report, ensure_ascii=False, sort_keys=True))

try:
    main()
except AssertionError as error:
    raise SystemExit(str(error)) from None
PY

トレードオフと失敗モード

legacy変更開始を決めるdecision table
観測 判断 追加証拠
経路とtestがありunknownが所有される 小さく可逆な変更へ進む 差分、rollback、利用者可視の結果
実行経路が空または推測だけ 編集を停止する entry point付きtraceとfixture
既存出力は観測したが仕様根拠がない behaviorを保存し正しさは断定しない 業務owner、履歴、production observation
  • 誤診: class名にDiscountがあるのでそこだけ直せばよい。反証: 入力付きexecution pathは共通subtotalと永続化を通り、名前検索だけではside effectを説明できない。
  • 誤診: characterization testがgreenなので現行動作は正しい。反証: testは固定fixtureで観測したactualを保存しただけで、業務仕様とproduction outcomeの妥当性は別途検証が必要である。
  • 誤診: unknownを記録すると理解不足を認めることになる。反証: unknownを推測から分離しownerと停止条件へ結ぶ方が、無根拠な全体理解より変更riskを制御できる。

知識チェック

  1. change request、execution path、change reasonはどの証拠で一貫させるか。
  2. characterization testのexpectedは何を意味し、何を意味しないか。
  3. unknownが一つ残る時、編集へ進む条件と停止する条件を分けよ。
  4. 割引率変更から税丸め変更へ要求だけが変わった時、再計算すべきartifactは何か。

出典と次の学習

SWEBOK Guide Version 4.0a(September 2025)とISO/IEC/IEEE 14764:2022をmaintenanceの共通語彙に使う。Software maintenance and evolution: a roadmap(2000)で研究課題の広がりを、Comprehension strategies and difficulties in maintaining object-oriented systems(2007)でprogram comprehensionの実証的な難しさを確認する。完全な書誌情報と参照先はlesson metadataに分離している。

次はcore-22で、この変更証拠を互換性のあるdatabase schema migrationとexpand-contractの停止・rollback判断へ接続する。

実践ラボ

legacy請求処理の変更前system mapを作る

提出成果物: 実行経路、変更理由、未知領域を示すシステム地図と特性テスト

  1. change requestを一つ選び、小さなsynthetic legacy fixtureのentry pointから実行経路を追跡する
  2. ownerまたは観測証拠がない領域をunknownとして残し、既存出力をcharacterization testへ固定する
  3. change requestだけを割引率変更から税丸め変更へ変え、影響経路と必要なtestを再構成する

説明して理解を確かめる

5分で、SWEBOK Guide Version 4.0aとISO/IEC/IEEE 14764:2022が扱うmaintenanceを、program comprehension研究と手元のexecution evidenceへどう接続するか説明する。

アセスメント

  1. 問い: 一つの関数名だけを見て影響範囲を確定する提案を評価せよ。

    期待する証拠: entry point、observed path、動的dispatch、data dependency、unknown、characterization test

  2. 問い: 既存出力が仕様上正しいと分からない時、何をtestへ固定し何を断定しないか。

    期待する証拠: observed behavior、fixture provenance、expectedとactual、限界、change reason、再調査条件

別問題へ転用する

割引率変更から税丸め変更へchange requestだけを変え、同じlegacy fixtureの影響経路、未知領域、特性テストを再構成する

復習スケジュール

  1. 1日後

    change requestからexecution pathを導く最小証拠は何か

  2. 7日後

    characterization testが正しい仕様の証明ではない理由は何か

  3. 30日後

    unknownを残すことと調査不足をどう区別するか

  4. 90日後

    change requestからexecution pathを導く最小証拠は何か

評価ルーブリック

4段階の評価基準
観点未達発展途上熟達卓越
technical-correctness名前検索だけで影響範囲を断定し実行証拠がない経路はあるがchange request、unknown、testとの対応が曖昧であるentry pointから影響経路、未知領域、characterization testを追跡できるdispatchとdata dependencyが変わっても地図の不変条件を検査できる
judgment理解できない領域を推測で埋めるunknownを挙げるが停止条件と追加調査を結ばない変更riskに応じて理解の深さ、停止条件、追加testを選ぶ業務影響と可逆性から調査投資と変更範囲を調整する
evidenceコードを読んだという記憶だけを根拠にするtestはあるが入力、観測経路、expectedの出自がない固定fixture、observed path、expected、actual、限界を再実行可能に残す要求変更ごとの差分と未知の解消履歴を監査可能にする
communicationlegacyは危険という抽象論だけを共有するcomponent一覧はあるがchange reasonと影響が読めない地図から変更理由、実行経路、unknown、testへ辿れる開発、運用、業務担当が同じ証拠で変更の停止と再開を判断できる

出典

以下の外部資料は利用者が選択したときだけ開きます。