0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

座席表の変更を安全にレビューする Python メタデータ比較

0
Posted at

座席表の変更を安全にレビューするための Python メタデータ比較

Seating plans are often reviewed visually, but visual review alone is weak when a layout changes repeatedly. A table may move only a few pixels while several assignments change. A guest may disappear from the visible area. A capacity adjustment may look harmless even though it removes the buffer needed for late arrivals. For engineering teams that store a plan as JSON, a small metadata comparison can make those changes easier to discuss without pretending to validate the real event.

This article builds a narrow Python utility that summarizes two plan documents and reports structural differences. It does not render a room, inspect image pixels, verify venue measurements, or decide whether any person should sit beside another person. The example intentionally stays at the metadata layer: table count, declared capacity, assigned and unassigned counts, and a stable digest of table-to-guest identifiers.

The motivating workflow is a web-based seating chart, but the pattern is useful for any reviewable configuration document. A planning team can export or serialize a draft, save a baseline, produce another draft, and compare the summaries before distributing the new version. The report is not an approval. It is a prompt for focused human review.

The project associated with this article provides a browser workflow at https://seating-chart-maker.org/. AI assistance was used to prepare and review this article. The code and fixtures below were executed locally, while the claims are deliberately limited to the demonstrated metadata contract. Nothing here proves that a venue is safe, accessible, correctly measured, or suitable for a particular group.

Define a small, explicit contract

A useful comparison starts by defining what it can observe. In this example, each table can have an identifier, a non-negative capacity, and a list of seats. A seat may carry a guest identifier. The plan can also list unassigned guest identifiers. Names, medical details, interpersonal notes, and other sensitive data are not required for this check.

That separation is important. A digest should not become a reason to copy unnecessary personal information into logs. Stable opaque identifiers are enough to detect that an assignment changed. The report can say that the assignment mapping differs without exposing the underlying planning notes.

The utility normalizes assignment rows before hashing them. Sorting by table identifier and guest identifier means that harmless JSON ordering changes do not produce a different digest. Moving the same guest from one table to another does produce a different digest because the normalized pair changes.

Declared capacities are clamped at zero for the summary. This does not repair invalid data and it does not replace schema validation. It simply prevents a negative fixture value from reducing the aggregate capacity. A production pipeline should validate its schema separately and decide whether invalid input should stop processing.

from __future__ import annotations

import hashlib
import json
from dataclasses import dataclass
from typing import Any, Mapping


@dataclass(frozen=True)
class PlanSummary:
    table_count: int
    assigned_count: int
    unassigned_count: int
    capacity_total: int
    assignment_digest: str


def _normalized_assignments(plan: Mapping[str, Any]) -> list[dict[str, str]]:
    rows: list[dict[str, str]] = []
    for table in plan.get("tables", []):
        table_id = str(table.get("id", "")).strip()
        for seat in table.get("seats", []):
            guest_id = str(seat.get("guest_id", "")).strip()
            if guest_id:
                rows.append({"table_id": table_id, "guest_id": guest_id})
    return sorted(rows, key=lambda row: (row["table_id"], row["guest_id"]))


def summarize_plan(plan: Mapping[str, Any]) -> PlanSummary:
    tables = list(plan.get("tables", []))
    assignments = _normalized_assignments(plan)
    capacity_total = sum(max(0, int(table.get("capacity", 0))) for table in tables)
    unassigned_count = len(
        {
            str(item).strip()
            for item in plan.get("unassigned_guest_ids", [])
            if str(item).strip()
        }
    )
    payload = json.dumps(
        assignments,
        ensure_ascii=False,
        separators=(",", ":"),
    )
    return PlanSummary(
        table_count=len(tables),
        assigned_count=len(assignments),
        unassigned_count=unassigned_count,
        capacity_total=capacity_total,
        assignment_digest=hashlib.sha256(payload.encode("utf-8")).hexdigest(),
    )


def compare_plan_metadata(
    before: Mapping[str, Any],
    after: Mapping[str, Any],
) -> dict[str, Any]:
    left = summarize_plan(before)
    right = summarize_plan(after)
    return {
        "table_count_delta": right.table_count - left.table_count,
        "assigned_count_delta": right.assigned_count - left.assigned_count,
        "unassigned_count_delta": right.unassigned_count - left.unassigned_count,
        "capacity_total_delta": right.capacity_total - left.capacity_total,
        "assignment_changed": left.assignment_digest != right.assignment_digest,
        "before_digest": left.assignment_digest,
        "after_digest": right.assignment_digest,
    }

Why a digest is useful but incomplete

The SHA-256 digest gives reviewers a compact answer to one question: did the normalized table-to-guest mapping change? It is deterministic for the fixture contract, and the sort step prevents list ordering from affecting the result. It is not encryption, authorization, a digital signature, or proof that the source document is trustworthy.

The digest also does not explain the change. When it differs, a reviewer still needs a detailed diff or the two source plans. For privacy-sensitive workflows, that detailed material should remain inside the appropriate system rather than being attached to a public issue or application log.

Counts provide useful context around the digest. If assigned count is unchanged but the digest differs, one or more people may have moved. If capacity rises while table count stays constant, a table definition probably changed. If unassigned count falls, assignments may have been added, but the utility does not prove that the intended people were assigned.

These observations are deliberately phrased as possibilities. Metadata can direct attention; it cannot infer the organizer’s intent or predict human experience. A balanced count does not mean that accessibility, safeguarding, communication, or venue constraints are satisfied.

Test the declared behavior with synthetic fixtures

The test suite below contains exactly seven unit tests. They use synthetic identifiers and do not contain real guest information. The cases cover empty input, capacity aggregation, negative capacity handling, blank guest identifiers, order-independent digests, assignment movement, and count deltas.

The order-independence test is especially valuable because exported JSON frequently changes list order. A noisy digest would train reviewers to ignore alerts. The movement test checks the opposite requirement: changing a table identifier for the same guest must alter the assignment digest.

The count-delta test keeps its assertion scope narrow. It checks arithmetic from the supplied fixture. It does not claim that adding a table is appropriate, that the capacities fit a real room, or that the resulting layout can be used safely.

import unittest

from seating_plan_diff import compare_plan_metadata, summarize_plan


class SeatingPlanDiffTests(unittest.TestCase):
    def test_empty_plan_has_zero_counts(self):
        self.assertEqual(summarize_plan({}).table_count, 0)
        self.assertEqual(summarize_plan({}).assigned_count, 0)

    def test_capacity_is_summed_from_tables(self):
        plan = {
            "tables": [
                {"id": "A", "capacity": 6},
                {"id": "B", "capacity": 8},
            ]
        }
        self.assertEqual(summarize_plan(plan).capacity_total, 14)

    def test_negative_capacity_is_not_counted(self):
        plan = {
            "tables": [
                {"id": "A", "capacity": -2},
                {"id": "B", "capacity": 4},
            ]
        }
        self.assertEqual(summarize_plan(plan).capacity_total, 4)

    def test_blank_guest_ids_are_ignored(self):
        plan = {
            "tables": [
                {
                    "id": "A",
                    "seats": [
                        {"guest_id": ""},
                        {"guest_id": "g1"},
                    ],
                }
            ]
        }
        self.assertEqual(summarize_plan(plan).assigned_count, 1)

    def test_assignment_digest_ignores_input_order(self):
        first = {
            "tables": [
                {"id": "B", "seats": [{"guest_id": "g2"}]},
                {"id": "A", "seats": [{"guest_id": "g1"}]},
            ]
        }
        second = {
            "tables": [
                {"id": "A", "seats": [{"guest_id": "g1"}]},
                {"id": "B", "seats": [{"guest_id": "g2"}]},
            ]
        }
        self.assertEqual(
            summarize_plan(first).assignment_digest,
            summarize_plan(second).assignment_digest,
        )

    def test_assignment_move_is_reported(self):
        before = {
            "tables": [
                {"id": "A", "seats": [{"guest_id": "g1"}]}
            ]
        }
        after = {
            "tables": [
                {"id": "B", "seats": [{"guest_id": "g1"}]}
            ]
        }
        self.assertTrue(
            compare_plan_metadata(before, after)["assignment_changed"]
        )

    def test_count_deltas_are_reported(self):
        before = {
            "tables": [{"id": "A", "capacity": 4}],
            "unassigned_guest_ids": ["g2"],
        }
        after = {
            "tables": [
                {"id": "A", "capacity": 6},
                {"id": "B", "capacity": 4},
            ],
            "unassigned_guest_ids": [],
        }
        result = compare_plan_metadata(before, after)
        self.assertEqual(result["table_count_delta"], 1)
        self.assertEqual(result["capacity_total_delta"], 6)
        self.assertEqual(result["unassigned_count_delta"], -1)


if __name__ == "__main__":
    unittest.main()

Run the test from a clean directory

Place the utility and test module in the same directory. The documented command uses the standard library test runner, so no third-party test framework is required.

python3 -m unittest -v test_seating_plan_diff.py

The local run completed seven tests successfully. That result verifies only the behavior represented by these fixtures and assertions. It does not test malformed JSON parsing, extremely large files, concurrent edits, real exports, browser storage, image decoding, venue data, or the seating application itself.

A useful engineering report records both the passing result and those omissions. Tests become misleading when a green result is presented as general product validation. Here, the suite demonstrates that the small summary functions behave as documented for the supplied in-memory mappings.

Integrate the summary into a review pipeline

One practical integration is a pre-release check for exported plan metadata. Save the previous approved export in controlled storage, create a new export, run the comparison, and attach the small summary to a review request. Keep the underlying plan private if it contains personal information.

A reviewer can then ask targeted questions. Why did total capacity fall by four? Was the new table intentional? Why did the assignment digest change when assigned count stayed constant? Does the unassigned count match the organizer’s current list? These questions are more actionable than a generic request to inspect the whole diagram again.

The comparison should not automatically approve or reject a plan. Some valid edits change every assignment. Some dangerous situations leave metadata unchanged. A room layout can have the same capacities and assignments while an aisle becomes too narrow. That physical fact is outside this model.

The same boundary applies to accessibility and safeguarding. The utility does not know why a placement was chosen, whether a participant consented to sharing information, or whether venue staff confirmed the arrangement. Those checks require appropriate people, current evidence, and direct communication.

Privacy and retention

Use opaque identifiers rather than names where possible. Avoid including notes about health, relationships, age, or behavior in a general diff. If the source system needs sensitive operational information, restrict access and keep it out of ordinary logs.

The digest is derived from identifiers, so it should still be treated as part of the plan’s audit data. Define a retention period. Delete old exports and reports when they no longer serve a legitimate operational purpose. Do not publish fixtures copied from a real event.

Logging should also avoid accidental source capture. A debug statement that prints the complete input mapping can defeat the privacy benefit of a metadata-only report. Log the summary and a controlled record identifier, not the full plan.

Failure modes to address before production use

The example accepts Python mappings and assumes expected value shapes. Production code should validate schemas and handle conversion failures explicitly. An invalid capacity string currently raises an exception. Duplicate table identifiers are not rejected. Duplicate guest assignments are counted rather than diagnosed. Those choices are visible limitations, not hidden guarantees.

Large documents need resource limits. The current implementation builds an assignment list and serializes it before hashing. A production version may need bounded input sizes and a streaming strategy, but streaming changes canonicalization design. Do not claim bounded memory simply because the final digest is small.

Version the normalization contract if it becomes durable. Changing sort rules, identifier trimming, or payload structure changes digests. A report should carry the comparison version so that two digests created by different implementations are not treated as directly comparable.

Finally, protect the baseline. Comparing an untrusted new plan with an untrusted baseline gives a precise answer to the wrong question. Access controls, audit history, and approval ownership remain necessary around the utility.

Conclusion

A metadata comparison is a modest tool with a useful role. It can expose count changes, distinguish harmless input ordering from assignment movement, and help reviewers focus their attention. Its value comes from a narrow contract and honest limits.

The example deliberately avoids deciding what a good seating arrangement looks like. It does not replace visual inspection, participant knowledge, venue confirmation, accessibility review, or a final operational walkthrough. Used as one layer in that broader process, a deterministic summary can make repeated seating-plan revisions easier to explain and safer to review.

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?