diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 2866608..c8e63d6 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -17,6 +17,7 @@ | workplan | RMGR-WP-0007 | finished | — | workplans/RMGR-WP-0007-greenfield-rapp-wrap-efficiency.md | | workplan | RMGR-WP-0008 | finished | — | workplans/RMGR-WP-0008-work-record-and-register-receiving-surface.md | | workplan | RMGR-WP-0009 | finished | — | workplans/RMGR-WP-0009-coding-assistant-commit-provenance.md | +| workplan | RMGR-WP-0010 | active | — | workplans/RMGR-WP-0010-authoritative-workload-references.md | | task | RMGR-WP-0001-T01 | done | — | workplans/RMGR-WP-0001-foundation.md | | task | RMGR-WP-0001-T02 | done | — | workplans/RMGR-WP-0001-foundation.md | | task | RMGR-WP-0001-T03 | done | — | workplans/RMGR-WP-0001-foundation.md | @@ -79,5 +80,12 @@ | task | RMGR-WP-0009-T05 | done | — | workplans/RMGR-WP-0009-coding-assistant-commit-provenance.md | | task | RMGR-WP-0009-T06 | done | — | workplans/RMGR-WP-0009-coding-assistant-commit-provenance.md | | task | RMGR-WP-0009-T07 | done | — | workplans/RMGR-WP-0009-coding-assistant-commit-provenance.md | +| task | RMGR-WP-0010-T01 | done | — | workplans/RMGR-WP-0010-authoritative-workload-references.md | +| task | RMGR-WP-0010-T02 | done | — | workplans/RMGR-WP-0010-authoritative-workload-references.md | +| task | RMGR-WP-0010-T03 | done | — | workplans/RMGR-WP-0010-authoritative-workload-references.md | +| task | RMGR-WP-0010-T04 | todo | — | workplans/RMGR-WP-0010-authoritative-workload-references.md | +| task | RMGR-WP-0010-T05 | todo | — | workplans/RMGR-WP-0010-authoritative-workload-references.md | +| task | RMGR-WP-0010-T06 | wait | — | workplans/RMGR-WP-0010-authoritative-workload-references.md | +| task | RMGR-WP-0010-T07 | wait | — | workplans/RMGR-WP-0010-authoritative-workload-references.md | | intake | RMGR-IN-0001 | open | — | intakes/intakes.md | | intake | RMGR-IN-0002 | open | — | intakes/intakes.md | diff --git a/docs/adr-004-authoritative-workload-declarations.md b/docs/adr-004-authoritative-workload-declarations.md new file mode 100644 index 0000000..92aefaf --- /dev/null +++ b/docs/adr-004-authoritative-workload-declarations.md @@ -0,0 +1,92 @@ +--- +id: RMGR-ADR-004 +type: architecture-decision-record +title: "Authoritative workload declarations and explicit consumer references" +status: accepted +decided: "2026-08-22" +deciders: ["Bernd Worsch", "repo-manager"] +related: + - RMGR-WP-0010 + - RMASTER-ADR-0007 + - ZONE-WP-0001-T03 + - docs/workload-reference-contract_v1.md +--- + +# ADR-004: Authoritative workload declarations and explicit consumer references + +## Status + +**Accepted** (2026-08-22). + +## Context + +Zone Engine needs an authoritative workload subject before it can resolve +security-zone membership. Its estate measurement found nine rapp declarations, +but only one of 27 ops-warden credential lanes joined to one. Inferring a +workload from `path_template`, `owner_repo`, or a repository slug would silently +turn missing declarations into invented identity. + +Railiance Master ADR-0007 already defines the family authority: every running +deployable belongs to exactly one rapp, while a rapp may contain multiple +deployables that share rollout and rollback fate. Its JSON schema is normative. +Repo Manager implements repository integration and must not fork that semantic +vocabulary. + +## Decision + +1. `rapp-*/declarations/rapp.yaml` is the authoritative declaration surface for + every managed running deployable, including operational and tooling + workloads that participate in controls. +2. There is no parallel declaration surface for managed workloads outside the + rapp family. A running unit awaiting rapp extraction is migration debt and + resolves as `unknown` to workload-based controls. +3. Not every operational subject is a workload. Human access, credential + patterns, broker actions, one-shot jobs, and infrastructure resources retain + their native actor, lane, activity, or resource identity. They must not be + represented as fake workloads merely to make a join total. +4. Authority is divided deliberately: + - Railiance Master owns the normative rapp/workload vocabulary and schema. + - Each rapp/package owner authors its declaration. + - Repo Manager owns discovery, validation integration, indexing, stable + reference resolution, and coverage findings. + - hub-core distributes normalized projections to cross-repository consumers. + - Consuming catalogs own their explicit pointers and never copy declaration + metadata as a second source. +5. A v1 workload reference is the pair `rapp_id` and + `workload_identity.name`. A consumer may additionally identify one + `composition.member_repos[].deployables[]` member. Repository names and path + segments are not reference fallbacks. +6. Resolution has three semantically distinct outcomes: + - `resolved`: exactly one authoritative declaration matches; + - `unknown`: the subject should be a workload but no unique declaration + resolves; + - `not-applicable`: the consumer explicitly declares that its subject is not + a workload. + Omission must not collapse these states. +7. Ops-warden may add an explicit workload reference to each applicable lane. + It owns that catalog field and its applicability marker; Repo Manager owns + resolution of the reference. Zone Engine consumes the resolved declaration + and the catalog pointer and performs no name or path inference. + +## Consequences + +- Operational and tooling runtimes need rapp coverage just like application + workloads. Their ownership repositories do not become package repositories. +- Existing pre-rapp runtimes may continue operating during migration, but + workload-based controls must expose their identity as unknown. +- Repo Manager can detect missing declarations in `rapp-*` repositories, + duplicate reference pairs, and deployables claimed by more than one rapp. +- Full estate coverage still requires a runtime/deployable observation source; + repository names alone cannot prove that every running unit is declared. +- Consumer catalogs remain pointer layers. A resolved reference gives access to + current classification, criticality, readiness, and placement without copying + those values. + +## Rejected alternatives + +- **A generic second `workloads.yaml`.** This would create two authorities for + the same managed deployable and contradict RMASTER-ADR-0007. +- **Path or repository-name inference.** Current `qonto`, `rapp-qonto`, and + `qonto-assistant` drift proves that convention is not identity. +- **Making every credential lane a workload.** Several lanes describe actors, + patterns, or control-plane operations rather than running deployables. diff --git a/docs/workload-reference-contract_v1.md b/docs/workload-reference-contract_v1.md new file mode 100644 index 0000000..9219a53 --- /dev/null +++ b/docs/workload-reference-contract_v1.md @@ -0,0 +1,75 @@ +--- +id: RMGR-CONTRACT-WORKLOAD-REFERENCE-0001 +type: contract +title: "Authoritative workload reference and resolution contract" +version: "1" +status: active +created: "2026-08-22" +updated: "2026-08-22" +workplan_task: RMGR-WP-0010-T02 +--- + +# Authoritative workload reference and resolution contract v1 + +## Authority + +The referenced record is authoritative at +`rapp-*/declarations/rapp.yaml`, under the normative Railiance Master rapp +schema. Repo Manager indexes and resolves that file; it does not redefine or +copy its semantic fields. + +## Reference + +```yaml +workload_ref: + rapp_id: rapp-issue-core + name: issue-core + deployable: issue-core # optional +``` + +`rapp_id` and `name` are required and must exactly equal `rapp_id` and +`workload_identity.name` in one declaration. `deployable`, when present, must +exactly equal a member of `composition.member_repos[].deployables[]` in that +same declaration. + +Names are case-sensitive canonical slugs. A resolver must not strip prefixes, +parse secret paths, consult `owner_repo`, or substitute a repository name. +Renaming either required field is a reference migration, not an alias inferred +by the reader. + +## Resolution outcomes + +| Outcome | Meaning | Consumer behavior | +| --- | --- | --- | +| `resolved` | Exactly one declaration matches, including the optional deployable. | Consume the authoritative declaration projection. | +| `unknown` | A workload reference is expected but missing, ambiguous, invalid, or unresolved. | Fail closed where policy requires identity; expose the reason. | +| `not-applicable` | The catalog owner asserts that the subject is not a workload. | Use the subject's native typed identity; do not call it unknown. | + +Repo Manager produces `resolved` or `unknown`. `not-applicable` is an explicit +consumer-catalog assertion because only that catalog knows what its lane or +control represents. Consumers must represent the distinction rather than using +an absent field for both states. + +## Commands + +```bash +rmgr workload index --root /home/worsch +rmgr workload resolve \ + --root /home/worsch \ + --rapp-id rapp-issue-core \ + --name issue-core \ + --deployable issue-core +``` + +The index reports missing rapp declarations, duplicate reference pairs, and +deployables claimed by more than one rapp. Resolution is read-only and returns +`unknown` instead of applying a naming heuristic. + +## Consumer obligations + +- Ops-warden owns its lane field and whether a lane is workload-applicable. +- Zone Engine consumes explicit references plus declaration projections. +- hub-core may transport the normalized records but does not become their + authority. +- Copied classification, criticality, readiness, or placement values are + caches and must retain source identity and observation metadata. diff --git a/docs/workload-reference-contract_v1.yaml b/docs/workload-reference-contract_v1.yaml new file mode 100644 index 0000000..bd2933a --- /dev/null +++ b/docs/workload-reference-contract_v1.yaml @@ -0,0 +1,35 @@ +contract: helixforge.workload-reference +version: 1 +status: active +authority: + semantic_schema: railiance-master/schemas/rapp.schema.json + record_pattern: rapp-*/declarations/rapp.yaml + indexer: repo-manager +reference: + required: + - rapp_id + - name + optional: + - deployable + source_fields: + rapp_id: rapp_id + name: workload_identity.name + deployable: composition.member_repos[].deployables[] + forbidden_fallbacks: + - repository_name + - owner_repo + - path_segment +resolution: + producer_states: + - resolved + - unknown + consumer_asserted_states: + - not-applicable + unknown_reasons: + - not_found + - ambiguous + - deployable_not_declared +consumer_rules: + ops-warden: owns lane reference and applicability + zone-engine: consumes references without identity inference + hub-core: transports normalized projection without assuming authority diff --git a/src/repo_manager/cli.py b/src/repo_manager/cli.py index b93882e..c33b184 100644 --- a/src/repo_manager/cli.py +++ b/src/repo_manager/cli.py @@ -365,6 +365,23 @@ def main(argv: list[str] | None = None) -> int: p_cache_export.add_argument("--output", required=True) p_cache_export.add_argument("--force", action="store_true") + p_workload = sub.add_parser( + "workload", + help="Index or resolve authoritative rapp workload declarations", + ) + workload_sub = p_workload.add_subparsers(dest="workload_command") + p_workload_index = workload_sub.add_parser( + "index", help="Index workload identities from rapp declarations" + ) + p_workload_index.add_argument("--root", default=".", help="Fleet root or one rapp repo") + p_workload_resolve = workload_sub.add_parser( + "resolve", help="Resolve one explicit workload reference without inference" + ) + p_workload_resolve.add_argument("--root", default=".", help="Fleet root or one rapp repo") + p_workload_resolve.add_argument("--rapp-id", required=True) + p_workload_resolve.add_argument("--name", required=True) + p_workload_resolve.add_argument("--deployable", default=None) + args = parser.parse_args(argv) if args.version or args.command in (None, "version"): @@ -812,6 +829,24 @@ def main(argv: list[str] | None = None) -> int: print(json.dumps(result, indent=2)) return 0 if result.get("ok") else 1 + if args.command == "workload": + if not args.workload_command: + p_workload.print_help() + return 2 + from repo_manager.workloads import index_workloads, resolve_workload + + if args.workload_command == "index": + result = index_workloads(Path(args.root)) + else: + result = resolve_workload( + Path(args.root), + rapp_id=args.rapp_id, + name=args.name, + deployable=args.deployable, + ) + print(json.dumps(result, indent=2)) + return 0 if result.get("ok") else 1 + parser.print_help() return 0 diff --git a/src/repo_manager/workloads.py b/src/repo_manager/workloads.py new file mode 100644 index 0000000..69b110b --- /dev/null +++ b/src/repo_manager/workloads.py @@ -0,0 +1,238 @@ +"""Read-only index and resolver for authoritative rapp workload declarations.""" + +from __future__ import annotations + +from dataclasses import asdict, dataclass +from pathlib import Path +from typing import Any + +import yaml + +REFERENCE_CONTRACT = "helixforge.workload-reference/v1" + + +@dataclass(frozen=True) +class WorkloadRecord: + rapp_id: str + name: str + declaration_repo: str + declaration_path: str + ownership_repo: str | None + readiness_state: str | None + data_classification: str | None + criticality: str | None + deployables: tuple[str, ...] + + def to_dict(self) -> dict[str, Any]: + data = asdict(self) + data["deployables"] = list(self.deployables) + data["reference"] = {"rapp_id": self.rapp_id, "name": self.name} + return data + + +def _error(code: str, path: Path, message: str) -> dict[str, str]: + return {"code": code, "path": str(path), "message": message} + + +def _package_dirs(root: Path) -> list[Path]: + root = root.expanduser().resolve() + if root.name.startswith("rapp-"): + return [root] + return sorted(path for path in root.glob("rapp-*") if path.is_dir()) + + +def _text(value: Any) -> str | None: + if not isinstance(value, str): + return None + stripped = value.strip() + return stripped or None + + +def index_workloads(root: Path) -> dict[str, Any]: + """Index authoritative declarations without inventing missing identity.""" + root = root.expanduser().resolve() + records: list[WorkloadRecord] = [] + errors: list[dict[str, str]] = [] + + for package_dir in _package_dirs(root): + declaration = package_dir / "declarations" / "rapp.yaml" + if not declaration.is_file(): + errors.append( + _error( + "declaration_missing", + declaration, + "rapp repository has no authoritative declarations/rapp.yaml", + ) + ) + continue + try: + raw = yaml.safe_load(declaration.read_text(encoding="utf-8")) or {} + except (OSError, yaml.YAMLError) as exc: + errors.append(_error("declaration_invalid", declaration, str(exc))) + continue + if not isinstance(raw, dict): + errors.append( + _error("declaration_invalid", declaration, "declaration root must be a mapping") + ) + continue + if raw.get("kind") != "managed-workload-package": + errors.append( + _error( + "declaration_kind_invalid", + declaration, + "kind must be managed-workload-package", + ) + ) + continue + + rapp_id = _text(raw.get("rapp_id")) + identity = raw.get("workload_identity") + name = _text(identity.get("name")) if isinstance(identity, dict) else None + if not rapp_id or not name: + errors.append( + _error( + "identity_missing", + declaration, + "rapp_id and workload_identity.name are required", + ) + ) + continue + if rapp_id != package_dir.name: + errors.append( + _error( + "rapp_repo_mismatch", + declaration, + f"rapp_id {rapp_id!r} does not match declaration repo {package_dir.name!r}", + ) + ) + continue + + deployables: list[str] = [] + composition = raw.get("composition") + members = composition.get("member_repos", []) if isinstance(composition, dict) else [] + if not isinstance(members, list): + errors.append( + _error( + "composition_invalid", + declaration, + "composition.member_repos must be a list", + ) + ) + continue + for member in members: + if not isinstance(member, dict) or not isinstance(member.get("deployables"), list): + errors.append( + _error( + "composition_invalid", + declaration, + "every composition member must declare a deployables list", + ) + ) + continue + deployables.extend( + value for value in (_text(item) for item in member["deployables"]) if value + ) + if not deployables: + errors.append( + _error( + "deployables_missing", + declaration, + "the workload declaration has no running deployables", + ) + ) + continue + + records.append( + WorkloadRecord( + rapp_id=rapp_id, + name=name, + declaration_repo=package_dir.name, + declaration_path=str(declaration.relative_to(root)), + ownership_repo=_text(raw.get("ownership_repo")), + readiness_state=_text(raw.get("readiness_state")), + data_classification=_text(raw.get("data_classification")), + criticality=_text(raw.get("criticality")), + deployables=tuple(sorted(set(deployables))), + ) + ) + + references: dict[tuple[str, str], list[WorkloadRecord]] = {} + deployable_owners: dict[str, list[WorkloadRecord]] = {} + for record in records: + references.setdefault((record.rapp_id, record.name), []).append(record) + for deployable in record.deployables: + deployable_owners.setdefault(deployable, []).append(record) + for reference, owners in sorted(references.items()): + if len(owners) > 1: + errors.append( + _error( + "reference_duplicate", + root, + f"workload reference {reference!r} resolves to {len(owners)} declarations", + ) + ) + for deployable, owners in sorted(deployable_owners.items()): + if len(owners) > 1: + errors.append( + _error( + "deployable_duplicate", + root, + f"deployable {deployable!r} belongs to more than one rapp: " + + ", ".join(sorted(owner.rapp_id for owner in owners)), + ) + ) + + return { + "ok": not errors, + "contract": REFERENCE_CONTRACT, + "root": str(root), + "declaration_count": len(records), + "workloads": [record.to_dict() for record in sorted(records, key=lambda r: r.rapp_id)], + "errors": errors, + } + + +def resolve_workload( + root: Path, + *, + rapp_id: str, + name: str, + deployable: str | None = None, +) -> dict[str, Any]: + """Resolve an explicit reference, returning unknown instead of guessing.""" + index = index_workloads(root) + matches = [ + record + for record in index["workloads"] + if record["rapp_id"] == rapp_id and record["name"] == name + ] + reference: dict[str, str] = {"rapp_id": rapp_id, "name": name} + if deployable: + reference["deployable"] = deployable + if len(matches) != 1: + return { + "ok": True, + "contract": REFERENCE_CONTRACT, + "status": "unknown", + "reference": reference, + "reason": "not_found" if not matches else "ambiguous", + "index_errors": index["errors"], + } + workload = matches[0] + if deployable and deployable not in workload["deployables"]: + return { + "ok": True, + "contract": REFERENCE_CONTRACT, + "status": "unknown", + "reference": reference, + "reason": "deployable_not_declared", + "index_errors": index["errors"], + } + return { + "ok": True, + "contract": REFERENCE_CONTRACT, + "status": "resolved", + "reference": reference, + "workload": workload, + "index_errors": index["errors"], + } diff --git a/tests/test_workloads.py b/tests/test_workloads.py new file mode 100644 index 0000000..f11184b --- /dev/null +++ b/tests/test_workloads.py @@ -0,0 +1,104 @@ +from __future__ import annotations + +from pathlib import Path + +from repo_manager.workloads import index_workloads, resolve_workload + + +def _declaration( + root: Path, + rapp_id: str, + name: str, + deployables: list[str], +) -> None: + path = root / rapp_id / "declarations" / "rapp.yaml" + path.parent.mkdir(parents=True) + rendered = "\n".join(f" - {item}" for item in deployables) + path.write_text( + f"""kind: managed-workload-package +rapp_id: {rapp_id} +ownership_repo: {name} +readiness_state: declared +workload_identity: + name: {name} +data_classification: confidential +criticality: high +composition: + member_repos: + - repo: {rapp_id} + deployables: +{rendered} +""", + encoding="utf-8", + ) + + +def test_indexes_authoritative_workload_identity_and_deployables(tmp_path: Path) -> None: + _declaration(tmp_path, "rapp-issue-core", "issue-core", ["issue-core"]) + + result = index_workloads(tmp_path) + + assert result["ok"] is True + assert result["declaration_count"] == 1 + assert result["workloads"][0]["reference"] == { + "rapp_id": "rapp-issue-core", + "name": "issue-core", + } + assert result["workloads"][0]["deployables"] == ["issue-core"] + + +def test_reports_missing_rapp_declaration_without_inventing_identity(tmp_path: Path) -> None: + (tmp_path / "rapp-undeclared").mkdir() + + result = index_workloads(tmp_path) + + assert result["ok"] is False + assert result["workloads"] == [] + assert result["errors"][0]["code"] == "declaration_missing" + + +def test_reports_deployable_owned_by_more_than_one_rapp(tmp_path: Path) -> None: + _declaration(tmp_path, "rapp-one", "one", ["shared-api"]) + _declaration(tmp_path, "rapp-two", "two", ["shared-api"]) + + result = index_workloads(tmp_path) + + assert result["ok"] is False + assert any(error["code"] == "deployable_duplicate" for error in result["errors"]) + + +def test_resolves_only_explicit_rapp_and_workload_pair(tmp_path: Path) -> None: + _declaration(tmp_path, "rapp-qonto", "qonto", ["qonto-assistant"]) + + resolved = resolve_workload( + tmp_path, + rapp_id="rapp-qonto", + name="qonto", + deployable="qonto-assistant", + ) + unknown = resolve_workload(tmp_path, rapp_id="rapp-qonto", name="rapp-qonto") + + assert resolved["status"] == "resolved" + assert resolved["workload"]["name"] == "qonto" + assert unknown == { + "ok": True, + "contract": "helixforge.workload-reference/v1", + "status": "unknown", + "reference": {"rapp_id": "rapp-qonto", "name": "rapp-qonto"}, + "reason": "not_found", + "index_errors": [], + } + + +def test_unknown_deployable_is_not_inferred_from_repository_name(tmp_path: Path) -> None: + _declaration(tmp_path, "rapp-openbao", "openbao", ["openbao-api"]) + + result = resolve_workload( + tmp_path, + rapp_id="rapp-openbao", + name="openbao", + deployable="rapp-openbao", + ) + + assert result["status"] == "unknown" + assert result["reason"] == "deployable_not_declared" diff --git a/workplans/RMGR-WP-0010-authoritative-workload-references.md b/workplans/RMGR-WP-0010-authoritative-workload-references.md new file mode 100644 index 0000000..8a79d42 --- /dev/null +++ b/workplans/RMGR-WP-0010-authoritative-workload-references.md @@ -0,0 +1,118 @@ +--- +id: RMGR-WP-0010 +type: workplan +title: "Authoritative workload declarations and explicit references" +domain: infotech +repo: repo-manager +status: active +owner: codex +topic_slug: infotech +created: "2026-08-22" +updated: "2026-08-22" +related: + - ZONE-WP-0001 + - RMASTER-ADR-0007 + - WARDEN-WP-0032 +--- + +# Authoritative workload declarations and explicit references + +## Goal + +Make authoritative workload identities resolvable across repository boundaries +without inventing them from paths or repository names, while preserving +Railiance Master schema ownership and consumer-catalog boundaries. + +## Record the authority ruling + +```task +id: RMGR-WP-0010-T01 +status: done +priority: high +``` + +Record the authoritative declaration surface, owner split, non-workload +boundary, and resolved/unknown/not-applicable semantics. + +**Result (2026-08-22):** accepted as `RMGR-ADR-004`. Railiance Master owns the +normative vocabulary; rapp repositories own their declarations; Repo Manager +owns integration and resolution; consumers own explicit references. + +## Publish the workload-reference contract + +```task +id: RMGR-WP-0010-T02 +status: done +priority: high +``` + +Define a stable reference that can be carried by pointer catalogs without +copying workload metadata or depending on a storage schema. + +**Result (2026-08-22):** `docs/workload-reference-contract_v1.{md,yaml}` defines +the `(rapp_id, workload_identity.name)` key, optional deployable, forbidden +fallbacks, and three resolution outcomes. + +## Implement read-only indexing and resolution + +```task +id: RMGR-WP-0010-T03 +status: done +priority: high +``` + +Index all checked-out rapp declarations, detect missing/duplicate authority, +and resolve exact references without inference. + +**Result (2026-08-22):** `rmgr workload index|resolve` indexes the nine current +declarations, rejects duplicate deployable ownership, reports missing rapp +declarations, and returns `unknown` rather than guessing. Unit tests cover +positive resolution and fail-closed cases. + +## Publish the normalized projection port + +```task +id: RMGR-WP-0010-T04 +status: todo +priority: medium +``` + +Expose the v1 index through the Repo Manager observation contract and hub-core +port with source revision, declaration path, and observation timestamp. Do not +expose private persistence models. + +## Add authoritative estate coverage + +```task +id: RMGR-WP-0010-T05 +status: todo +priority: high +``` + +Join declarations to an authoritative runtime/deployable observation source and +report running units lacking rapp coverage. Repository names are insufficient +evidence. Separate non-workload operational subjects from undeclared workloads. + +## Adopt explicit ops-warden references + +```task +id: RMGR-WP-0010-T06 +status: wait +priority: high +``` + +Ops-warden owns its catalog schema. Add an explicit workload reference and an +unambiguous not-applicable/unknown distinction, then validate every resolved +reference through Repo Manager. Do not copy classification or criticality. + +## Prove Zone Engine consumption + +```task +id: RMGR-WP-0010-T07 +status: wait +priority: high +``` + +Zone Engine consumes the declaration projection and explicit catalog references, +reports unknown subjects without inference, and demonstrates that path/repo-name +drift cannot alter membership.