diff --git a/config/notion-project-map.json b/config/notion-project-map.json index d55b9d4..7420cdb 100644 --- a/config/notion-project-map.json +++ b/config/notion-project-map.json @@ -11,6 +11,12 @@ "portfolio-index": { "localProjectId": "378c21f1-caf0-814e-b127-ddbfe578e1e5" }, + "operating-memory-os": { + "localProjectId": "3a3c21f1-caf0-81ec-943f-ed25069652e2" + }, + "operator-scripts": { + "localProjectId": "3a3c21f1-caf0-810e-b671-d7cba2618f41" + }, "portfolio-mcp": { "localProjectId": "38dc21f1-caf0-8157-9f5c-d6425a4b32b9" }, diff --git a/config/project-registry-overrides.json b/config/project-registry-overrides.json index 66f495e..7b8acae 100644 --- a/config/project-registry-overrides.json +++ b/config/project-registry-overrides.json @@ -30,6 +30,14 @@ "lifecycle_state": "active", "note": "Local operator control plane (127.0.0.1:46210). Most active project in bridge-db yet absent from auditor portfolio-truth." }, + { + "canonical_key": "operator-scripts", + "display_name": "operator-scripts", + "repo_full_name": "saagpatel/operator-scripts", + "group_key": "operator_infra", + "lifecycle_state": "active", + "note": "Private scheduled-job scripts repository rooted at /Users/d/scripts, outside the default /Users/d/Projects audit scan." + }, { "canonical_key": "supp:SecondBrain", "display_name": "SecondBrain", diff --git a/src/operator_os_seam_linter.py b/src/operator_os_seam_linter.py index e9c8f70..4894165 100644 --- a/src/operator_os_seam_linter.py +++ b/src/operator_os_seam_linter.py @@ -22,9 +22,9 @@ BRIDGE_CANONICAL_KEY_DISAGREEMENTS, DEFAULT_NOTION_PROJECTION_ONLY_ROWS, DEFAULT_NOTION_TITLE_ALIASES, - DEFAULT_SUPPLEMENTARY, IDENTITY_ALIAS_MAP, IDENTITY_ALIAS_MAP_DEPRECATES_AFTER, + load_overrides_config, normalize, supp_key_for, ) @@ -38,6 +38,9 @@ DEFAULT_NOTION_SNAPSHOT_PATH = Path( "~/.local/share/notion-os/project-snapshot.json" ).expanduser() +DEFAULT_PROJECT_REGISTRY_OVERRIDES_PATH = ( + Path(__file__).resolve().parents[1] / "config" / "project-registry-overrides.json" +) HEX_FRAGMENT_RE = re.compile(r"^[0-9a-fA-F]{3}$") EXPLICIT_UNRESOLVED_IDENTITIES = {"homeadhoc", "unresolved"} @@ -792,14 +795,22 @@ def _build_identity_resolver(truth: dict[str, Any]) -> dict[str, str]: if supp: for value in (identity.get("display_name"), project_key, supp): _add_identity_alias(resolver, value, supp) - # Supplementary registry projects (repo-less operator-OS projects the - # auditor's portfolio-truth does not track, e.g. personal-ops, SecondBrain) - # carry a supp: canonical_key. Seed them so they resolve like any project. - for supp_entry in DEFAULT_SUPPLEMENTARY: + # Supplementary registry projects (operator-OS projects the auditor's + # portfolio-truth does not track) must come from the same operator-owned + # config consumed by the registry generator. Falling back to built-ins + # preserves degraded operation when that config is absent. + _, supplementary, *_ = load_overrides_config( + DEFAULT_PROJECT_REGISTRY_OVERRIDES_PATH + ) + for supp_entry in supplementary: canonical = supp_entry.get("canonical_key") if not isinstance(canonical, str) or not canonical: continue - for value in (supp_entry.get("display_name"), canonical): + for value in ( + supp_entry.get("display_name"), + supp_entry.get("repo_full_name"), + canonical, + ): _add_identity_alias(resolver, value, canonical) # Notion title aliases (e.g. "OrbitForge (staging)" -> OrbitForge): resolve # the alias target to its canonical, then map the drifted title onto it so diff --git a/tests/test_operator_os_seam_linter.py b/tests/test_operator_os_seam_linter.py index 6d0cf15..ac65932 100644 --- a/tests/test_operator_os_seam_linter.py +++ b/tests/test_operator_os_seam_linter.py @@ -456,6 +456,28 @@ def test_identity_resolution_supplementary_project_resolves(tmp_path: Path) -> N assert result.passed, [f.detail for f in result.findings] +def test_identity_resolution_configured_supplementary_repo_resolves( + tmp_path: Path, +) -> None: + # operator-scripts is enrolled only in the operator-owned supplementary + # config, not the built-in fallback. Both bridge identity dialects must + # resolve through the same source used by the registry generator. + truth, markdown = _passing_paths(tmp_path) + _write_identity_truth(truth) + bridge_db = tmp_path / "bridge.db" + _write_bridge_db( + bridge_db, + activity_rows=[ + ("operator-scripts", "saagpatel/operator-scripts"), + ("saagpatel/operator-scripts", "saagpatel/operator-scripts"), + ], + ) + result = lint_operator_os_seams( + truth_path=truth, markdown_paths=markdown, bridge_db_path=bridge_db, now=NOW + ) + assert result.passed, [f.detail for f in result.findings] + + def test_identity_resolution_migration_drift_alias_resolves(tmp_path: Path) -> None: # o2-fable-runpack is OPERANT activity logged under a loose name; the # census-seeded alias map maps it to the operant repo so it resolves.