A category number can identify different things in an editorial library and an established website. This independent helper applies an explicit mapping without guessing the meaning of either catalog or changing a site.

Four separate inputs

project_categories receives the existing public categories, explicit mapping rules, existing route strings and incoming article descriptors. Public categories have exactly id, slug and meaning fields. Rules have source_id, target_id, target_slug and meaning. Articles have id, source_category_id and route. Nonempty strings are bounded to 160 characters, with at most 64 categories or rules and 128 route strings or articles.

The caller must prepare the existing inventory from the relevant site. This function does not visit a site or discover files. An omitted existing route cannot be protected by comparing a list that does not contain it. Establishing a complete input inventory and deciding the intended meanings are integration responsibilities, rather than behavior supplied by this helper.

Check both IDs and slugs

source_id names the editorial side of a rule. target_id and target_slug identify its public destination. An occupied public ID with a different declared target causes PUBLIC_ID_CONFLICT. An occupied slug associated with a different target causes PUBLIC_SLUG_CONFLICT. Checking the numeric or textual ID alone would leave a second collision surface unchecked.

An existing target can be reused only when its id, slug and meaning declarations are exactly equal. Multiple source categories can converge on that same declaration. Equality here is deliberately limited: meaning is supplied metadata, not an independently authenticated description of the business. The helper compares strings; it does not read the articles and determine whether the categories truly belong together.

Retain origin and route in the projection

Each projected article keeps its original source_category_id and route, and gains public_category_id and public_category_slug. Replacing the source category would erase part of the explanation for the projection. Existing public category rows retain their order and values, while new target declarations are appended. Detached output values prevent later editing of the result from mutating the supplied catalog.

An incoming route string that matches an existing route or another incoming route produces ARTICLE_ROUTE_CONFLICT. Duplicate incoming article IDs and missing source rules have separate errors. A route is an opaque comparison string in this contract. URL normalization, relative path generation, filesystem containment and actual HTTP checks belong in a separate route and publication layer.

Explore the implementation

Complete independent synthetic implementation

"""Bounded, synthetic namespace projection. No IO, semantic inference or publishing."""
from copy import deepcopy


def project_categories(existing_categories, rules, existing_routes, articles):
    def rows(value, limit):
        if type(value) is not list or len(value) > limit:
            raise ValueError('BOUNDED_LIST')
        return value

    def fields(row, names):
        if type(row) is not dict or set(row) != set(names):
            raise ValueError('EXACT_FIELDS')
        for value in row.values():
            if type(value) is not str or not value.strip() or len(value) > 160:
                raise ValueError('BOUNDED_NONEMPTY_TEXT')

    categories, by_id, by_slug = [], {}, {}
    for row in rows(existing_categories, 64):
        fields(row, ('id', 'slug', 'meaning'))
        if row['id'] in by_id or row['slug'] in by_slug:
            raise ValueError('DUPLICATE_PUBLIC_CATEGORY')
        clone = deepcopy(row)
        categories.append(clone)
        by_id[row['id']] = clone
        by_slug[row['slug']] = clone

    mapping = {}
    for row in rows(rules, 64):
        fields(row, ('source_id', 'target_id', 'target_slug', 'meaning'))
        if row['source_id'] in mapping:
            raise ValueError('DUPLICATE_SOURCE_RULE')
        target = {'id': row['target_id'], 'slug': row['target_slug'], 'meaning': row['meaning']}
        if row['target_id'] in by_id and by_id[row['target_id']] != target:
            raise ValueError('PUBLIC_ID_CONFLICT')
        if row['target_slug'] in by_slug and by_slug[row['target_slug']] != target:
            raise ValueError('PUBLIC_SLUG_CONFLICT')
        if row['target_id'] not in by_id:
            categories.append(target)
            by_id[row['target_id']] = target
            by_slug[row['target_slug']] = target
        mapping[row['source_id']] = target

    routes = set()
    for value in rows(existing_routes, 128):
        if type(value) is not str or not value.strip() or len(value) > 160:
            raise ValueError('BOUNDED_NONEMPTY_TEXT')
        if value in routes:
            raise ValueError('DUPLICATE_EXISTING_ROUTE')
        routes.add(value)

    projected, ids = [], set()
    for row in rows(articles, 128):
        fields(row, ('id', 'source_category_id', 'route'))
        if row['id'] in ids:
            raise ValueError('DUPLICATE_ARTICLE_ID')
        if row['route'] in routes:
            raise ValueError('ARTICLE_ROUTE_CONFLICT')
        if row['source_category_id'] not in mapping:
            raise ValueError('UNMAPPED_SOURCE_CATEGORY')
        ids.add(row['id']); routes.add(row['route'])
        target = mapping[row['source_category_id']]
        projected.append({**deepcopy(row), 'public_category_id': target['id'],
                          'public_category_slug': target['slug']})

    return deepcopy({
        'categories': categories, 'articles': projected,
        'mapping': [{'source_category_id': source, 'public_category_id': target['id']}
                    for source, target in mapping.items()],
        'performs_write_or_publish': False,
        'authenticates_semantics': False,
    })

Reject a conflict instead of inventing a new assignment

The helper never searches for the next unused category number, changes case or guesses a missing mapping. A conflict is a reason to review the supplied rule. Preserving the original inputs makes it possible to inspect that decision again instead of accepting a silently repaired assignment. If normalization is needed, it should be a separately specified transformation with its own evidence.

The 24 tests cover ID and slug conflicts, reuse of an existing target, convergence from multiple sources, missing mappings, duplicate rules, articles and routes, bounded inputs, exact fields, input preservation and detached output. They exercise this independent synthetic implementation. They do not execute the original publisher, establish content accuracy or authorize any public release.

Inspect the actual synthetic records

The five records contain one successful explicit remap and rejections for an ID conflict, a slug conflict, a route conflict and a missing mapping. Each was executed locally using invented categories and article identifiers. In the successful result the existing section-a and new section-b coexist, while the article still identifies section-a as its source category. Error records make no publication claim.

The selector below displays stored JSON. It does not execute Python in the visitor’s browser or modify a CMS. Its record selection behavior is checked with DOM stubs, separately from rendered layout, keyboard use or responsive screen review. A real integration should add fixtures representing its own catalog, route inventory and intended mapping decisions.

A read-only viewer of synthetic local records

Before integrating the ZIP

The ZIP contains the complete bridge.py implementation, 24 tests in test_bridge.py, examples.py, five execution records and a README. It uses the Python standard library and permits modification and reuse. Run python -m unittest -v for the tests and python examples.py to regenerate the invented usage records. There is no deployment command in the package.

Before publication, separately connect semantic review, a complete existing inventory, route generation, content review, release authorization and post-publication verification. performs_write_or_publish and authenticates_semantics remain false. The existing route module linked below can help build and reconcile relative document paths; it has a different responsibility from projecting a source category into a public catalog.

Download code, tests and records
Python standard library, synthetic data, modification and reuse permitted