diff --git a/intelligence_radar/__main__.py b/intelligence_radar/__main__.py index b55f313..5b741f2 100644 --- a/intelligence_radar/__main__.py +++ b/intelligence_radar/__main__.py @@ -194,6 +194,8 @@ def main(argv=None): briefs_command.add_argument('--inbox', type=Path, help='override the configured inbox for this run') pull_command = commands.add_parser('pull-briefs', help='retain briefs that inbound-library routed to this repo') pull_command.add_argument('--config', type=Path, default=Path('ops/schedule.json')) + contribute_command = commands.add_parser('contribute-sources', help='offer catalog sources to inbound-library as shared pointers') + contribute_command.add_argument('--config', type=Path, default=Path('ops/schedule.json')) publish_command = commands.add_parser('publish', help='record a reviewed publication linked to captures') publish_command.add_argument('input', type=Path) commands.add_parser('health', help='show durable scheduler health') @@ -268,6 +270,13 @@ def main(argv=None): result['native'] = native_status() print(json.dumps(result, indent=2)) return 0 if result['ok'] else 1 + elif args.command == 'contribute-sources': + from .library_briefs import contribute_sources + spec = json.loads(args.config.read_text()).get('library') + if not spec: + print('no "library" section in the schedule config', file=sys.stderr) + return 2 + print(json.dumps(contribute_sources(args.root, spec), indent=2)) elif args.command == 'pull-briefs': from .library_briefs import pull spec = json.loads(args.config.read_text()).get('library') diff --git a/intelligence_radar/library_briefs.py b/intelligence_radar/library_briefs.py index a4759f7..9f754f2 100644 --- a/intelligence_radar/library_briefs.py +++ b/intelligence_radar/library_briefs.py @@ -166,3 +166,41 @@ def _write_markdown(root, provider, parsed, text, item, imported_at, subject, re f'- Imported: {imported_at}', '', text if text.endswith('\n') else text + '\n']) target.write_text(body, encoding='utf-8') return target.relative_to(root).as_posix() + + +def contribute_sources(root, spec): + """Write this repo's primary sources into the inbound-library registry as shared + URL pointers. URL and publisher come from catalog/sources.json; topics come from + spec["contribute"]. Primary capture stays here; the library does not fetch them. + + A file that this repo already wrote is updated in place. A file owned by another + repo is refused. + """ + root = Path(root) + library = Path(spec['root']).expanduser() + target = library / 'registry' / 'sources' + if not target.is_dir(): + raise LibraryUnavailable(f'inbound-library registry not found at {target}') + catalog = {s['id']: s for s in json.loads((root / 'catalog' / 'sources.json').read_text())['sources']} + owner = spec.get('repo', 'intelligence-radar') + written = [] + for ident, topics in sorted(spec.get('contribute', {}).items()): + if ident not in catalog: + raise ValueError(f'contribute names unknown catalog source {ident}') + entry = catalog[ident] + source_id = f'ir-{ident}' + record = {'id': source_id, 'owner_repo': owner, 'channel': 'url', 'url': entry['url'], + 'topics': sorted(topics), 'cadence': 'irregular', 'visibility': 'shared', + 'publisher': entry.get('publisher', ''), + 'notes': f'Primary source captured by {owner} (catalog/sources.json id ' + f'{ident}). Pointer only; inbound-library does not fetch it.'} + path = target / f'{source_id}.json' + if path.exists() and json.loads(path.read_text()).get('owner_repo') != owner: + raise ValueError(f'{path.name} belongs to another repo') + content = json.dumps(record, indent=2, ensure_ascii=False) + '\n' + status = 'unchanged' if path.exists() and path.read_text() == content else \ + ('updated' if path.exists() else 'created') + if status != 'unchanged': + atomic(path, content.encode()) + written.append({'source': source_id, 'status': status}) + return written diff --git a/ops/schedule.json b/ops/schedule.json index 5e29cde..5ac8525 100644 --- a/ops/schedule.json +++ b/ops/schedule.json @@ -7,7 +7,14 @@ "library": { "root": "~/inbound-library", "repo": "intelligence-radar", - "sources": {"grok-briefs-via-founder": "grok"} + "sources": {"grok-briefs-via-founder": "grok"}, + "contribute": { + "swe-bench": ["ai.benchmarks"], + "anthropic-pricing": ["ai.pricing"], + "anthropic-sonnet": ["ai.models"], + "anthropic-sonnet-launch": ["ai.models"], + "openrouter-models": ["ai.models", "ai.pricing"] + } }, "jobs": [ {"id": "radar-daily", "cadence": "daily", "at": "21:30", "sources": "catalog/sources.json"}, diff --git a/tests/test_library_briefs.py b/tests/test_library_briefs.py index 07defd0..ab19586 100644 --- a/tests/test_library_briefs.py +++ b/tests/test_library_briefs.py @@ -118,5 +118,39 @@ class LibraryBriefs(unittest.TestCase): ('daily', '2026-10-06', 'complete')) +class ContributeSources(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.root = Path(self.tmp.name) / 'radar' + self.library = Path(self.tmp.name) / 'library' + (self.root / 'catalog').mkdir(parents=True) + (self.library / 'registry' / 'sources').mkdir(parents=True) + (self.root / 'catalog' / 'sources.json').write_text(json.dumps({'sources': [ + {'id': 'swe-bench', 'publisher': 'SWE-bench maintainers', + 'url': 'https://www.swebench.com/'}]})) + self.spec = {'root': str(self.library), 'repo': 'intelligence-radar', + 'contribute': {'swe-bench': ['ai.benchmarks']}} + + def tearDown(self): + self.tmp.cleanup() + + def test_create_then_unchanged_and_pointer_shape(self): + self.assertEqual(library_briefs.contribute_sources(self.root, self.spec), + [{'source': 'ir-swe-bench', 'status': 'created'}]) + self.assertEqual(library_briefs.contribute_sources(self.root, self.spec), + [{'source': 'ir-swe-bench', 'status': 'unchanged'}]) + source = json.loads((self.library / 'registry' / 'sources' / 'ir-swe-bench.json').read_text()) + self.assertEqual((source['channel'], source['url'], source['visibility'], source['topics']), + ('url', 'https://www.swebench.com/', 'shared', ['ai.benchmarks'])) + + def test_refuses_another_repos_file_and_unknown_source(self): + (self.library / 'registry' / 'sources' / 'ir-swe-bench.json').write_text( + json.dumps({'id': 'ir-swe-bench', 'owner_repo': 'markitect'})) + with self.assertRaises(ValueError): + library_briefs.contribute_sources(self.root, self.spec) + with self.assertRaises(ValueError): + library_briefs.contribute_sources(self.root, {**self.spec, 'contribute': {'nope': []}}) + + if __name__ == '__main__': unittest.main() diff --git a/workplans/IR-WP-0010-inbound-library-briefs.md b/workplans/IR-WP-0010-inbound-library-briefs.md index 1ad3d3d..4e39305 100644 --- a/workplans/IR-WP-0010-inbound-library-briefs.md +++ b/workplans/IR-WP-0010-inbound-library-briefs.md @@ -132,7 +132,7 @@ reports both as duplicates. ```task id: IR-WP-0010-T04 -status: todo +status: done priority: medium state_hub_task_id: "54945f3f-d651-5de2-8e03-c554ab34dc2c" ``` @@ -145,6 +145,21 @@ re-ingest them. Acceptance: a second repo's subscription matches at least one contributed source. +Done 2026-10-06: +- `contribute-sources` writes the five catalog sources into inbound-library as + shared URL pointers. They are `ir-swe-bench`, `ir-anthropic-pricing`, + `ir-anthropic-sonnet`, `ir-anthropic-sonnet-launch` and `ir-openrouter-models`. + Their topics are `ai.benchmarks`, `ai.pricing` and `ai.models`, mapped in + `ops/schedule.json` under `library.contribute`. +- URL and publisher come from `catalog/sources.json`. Reruns are idempotent, + and a file owned by another repo is refused. Primary capture stays here; the + library lists the sources and does not fetch them. +- The library gained the `sources --repo` discovery command. +- Acceptance was verified against a temporary copy of the registry: a + hypothetical `llm-connect` subscription to `ai.pricing` matched + `ir-anthropic-pricing` and `ir-openrouter-models`. No real second consumer + is subscribed yet, and none was registered on another repo's behalf. + ## Run a week end to end, then retire the website-pull record ```task