diff --git a/README.md b/README.md index 5303ca3..9db9132 100644 --- a/README.md +++ b/README.md @@ -154,15 +154,31 @@ The Source knowledge graph is intentionally topic-scoped: every node on `/map` i ### Running a content top-up -From `sites/source/scripts/`: +From `scripts/`: ```bash -# Each script is idempotent — rerunning creates no duplicates. -# All four hit the public POST /api/pact/topics endpoint (no admin secret). -python seed_defence_au.py -python seed_defence_us.py -python seed_critical_minerals.py -python seed_topic_dependencies.py # run LAST — depends on topic IDs from the first three +# Each script is idempotent — rerunning creates no duplicates and registers +# no agent for a topic that already exists. All four hit the public +# POST /api/pact/topics and /dependencies endpoints (no admin secret). +# +# 1. Plan first (GET-only, nothing written, nothing registered): +SEED_DRY_RUN=1 python seed_defence_au.py +# +# 2. Apply, one script at a time, checking "N/N topics in place" before the next. +# Every canonicalClaim is linted against the server's atomic-claim rule +# (<= 140 chars, one sentence, no bundled and/or, no hedges) BEFORE any +# agent is registered; a failing corpus exits 2 with nothing written. +# Registration is limited to 60 per hour per address and each registration +# costs two requests, so a 30-topic run spends the whole hour's budget: +# do not re-run inside the hour, and keep the keys for step 3. +PACT_SEED_KEYS_FILE=/secure/path/seed-keys python seed_defence_au.py +PACT_SEED_KEYS_FILE=/secure/path/seed-keys python seed_defence_us.py +PACT_SEED_KEYS_FILE=/secure/path/seed-keys python seed_critical_minerals.py +# +# 3. Edges LAST — needs the topic IDs from the first three. With a keys file +# (or PACT_SEED_AGENT_KEY) it reuses a topic-script agent instead of +# registering a 31st; keep that file outside the repository and delete it after. +PACT_SEED_KEYS_FILE=/secure/path/seed-keys python seed_topic_dependencies.py # Structured legislation ingest (requires ADMIN_SECRET / X-Admin-Key) python seed_sa_tas_legislation.py # SA/TAS industrial, WHS, environment, resources @@ -170,7 +186,7 @@ python seed_qld_liquor_legislation.py # Liquor Act 1992 (Qld) + Liquor Regulat python seed_qld_lga_legislation.py # Local Government Act 2009 (Qld) — council competence framework (#5117) ``` -Against a different env: `export BASE=https://source-dev.tailor.au` (default `https://source.tailor.au`). +Against a different env: `export SOURCE_BASE=http://localhost:3000` (default `https://pact.tailor.au`). ### Council-instrument class (#5117) diff --git a/scripts/_defence_seed_helpers.py b/scripts/_defence_seed_helpers.py index 1c2f427..20f4620 100644 --- a/scripts/_defence_seed_helpers.py +++ b/scripts/_defence_seed_helpers.py @@ -13,6 +13,7 @@ * Safe to re-run: second run creates zero new rows, only prints EXISTS messages. """ import os +import re import sys import time from typing import Optional @@ -34,6 +35,32 @@ # written. Prints CREATE / EXISTS per item so the run can be diffed against # the live graph before anything is applied (tailor-group#7, after #5581). DRY_RUN = os.environ.get("SEED_DRY_RUN", "").strip().lower() in ("1", "true", "yes") +# Optional: append every minted agent key (one per line, file mode 0600) so a later +# script — seed_topic_dependencies.py — can reuse one instead of registering again. +# Registration is 60/hour per address and each registration costs two hits, so a full +# 30-topic run leaves no budget for a 31st agent (tailor-group#7). Never commit this file. +KEYS_FILE = os.environ.get("PACT_SEED_KEYS_FILE", "").strip() + + +def persist_key(api_key: str) -> None: + """Append a freshly minted key to PACT_SEED_KEYS_FILE (no-op when unset).""" + if not KEYS_FILE: + return + fd = os.open(KEYS_FILE, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600) + with os.fdopen(fd, "a") as fh: + fh.write(api_key + "\n") + + +def reusable_key() -> Optional[str]: + """PACT_SEED_AGENT_KEY, else the last key in PACT_SEED_KEYS_FILE, else None.""" + direct = os.environ.get("PACT_SEED_AGENT_KEY", "").strip() + if direct: + return direct + if KEYS_FILE and os.path.isfile(KEYS_FILE): + lines = [ln.strip() for ln in open(KEYS_FILE, encoding="utf-8") if ln.strip()] + if lines: + return lines[-1] + return None def api(method: str, path: str, key: Optional[str] = None, data: Optional[dict] = None, silent_429: bool = False): @@ -95,6 +122,7 @@ def register_agents(prefix: str, count: int) -> list[str]: time.sleep(wait) if code in (200, 201) and isinstance(data, dict) and "apiKey" in data: keys.append(data["apiKey"]) + persist_key(data["apiKey"]) print(f" registered {name}") else: err = data.get("error", str(data)[:140]) if isinstance(data, dict) else str(data)[:140] @@ -150,7 +178,16 @@ def create_topic(key: str, payload: dict, retry_on_civic: bool = True) -> tuple[ return data.get("id"), "CREATED" if code == 409 and isinstance(data, dict) and data.get("existingTopicId"): - return data["existingTopicId"], "EXISTS" + # The server 409s for an exact-title duplicate (no existingTitle) and + # for a fuzzy 75%-overlap near-duplicate (existingTitle = the OTHER + # topic's title). Only the former is "our topic already exists". + existing_title = data.get("existingTitle") + err = str(data.get("error", "")) + if existing_title is None and "exact title" in err: + return data["existingTopicId"], "EXISTS" + if isinstance(existing_title, str) and existing_title.strip().lower() == str(payload.get("title", "")).strip().lower(): + return data["existingTopicId"], "EXISTS" + return None, f"FAIL: 409 near-duplicate of a different topic {data['existingTopicId']}: {existing_title!r}" if code == 403 and retry_on_civic and isinstance(data, dict) and data.get("votesNeeded"): # Agent has created other topics but not voted enough. Fulfil duty + retry once. @@ -186,6 +223,68 @@ def find_topic_id_by_title(title: str) -> Optional[str]: return None +# ── Client-side mirror of src/lib/claim.ts lintAtomicClaim (tailor-group#7) ── +# The server rejects a canonicalClaim over 140 UTF-16 code units (JS .length), +# with more than one sentence (ANY internal '.', '!' or '?' counts, so no +# 'Rule 3.1', 's 5.6', 'e.g.', '252.225-7052' — section numbers belong in +# sourceRef), with a top-level and/or joining two verb-bearing clauses, or with +# a motte-and-bailey hedge. It lints AFTER sanitizeContent() strips control +# characters and HTML tags, so the mirror cleans the same way first. The first live apply of these corpora lost all 30 +# topics to that rule after 30 agents had already been registered; lint here +# so a dry-run fails on the claim, before any registration. +CANONICAL_CLAIM_MAX = 140 +# re.A: the server's regexes are non-unicode JS, so \b and /i are ASCII-only there. +_CLAUSE_CONJUNCTION = re.compile(r"\b(?:and|or)\b", re.I | re.A) +_VERB_HINT = re.compile( + r"\b(?:is|are|was|were|has|have|had|does|do|did|can|cannot|must|shall|should|will|would|may|might|" + r"equals|contains|requires|prohibits|permits|applies|boils|melts|freezes|rises|falls|exceeds|measures|" + r"weighs|holds|states|provides|mandates|forbids|bans|allows|increased|decreased|causes|caused)\b", re.I | re.A) +_HEDGES = [re.compile(p, re.I | re.A) for p in ( + r"\barguably\b", r"\bsome (?:might|may|would) (?:say|argue|claim)\b", r"\bit could be (?:said|argued)\b", + r"\bin some sense\b", r"\bmore or less\b", r"\bbasically\b", r"\bsort of\b|\bkind of\b")] + + +# ECMAScript WhiteSpace + LineTerminator, the set String.prototype.trim removes +# (U+0085 is NOT in it; U+FEFF is). +_JS_WS = "\t\n\x0b\x0c\r \xa0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000\ufeff" + + +def _server_clean(claim: str) -> str: + """sanitizeContent() as src/lib/sanitize.ts does it, then JS String.prototype.trim.""" + s = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]", "", claim or "") + s = re.sub(r"<[^>]*>", "", s) + return s.strip(_JS_WS) + + +def lint_atomic_claim(claim: str) -> Optional[str]: + """Return an error string mirroring the server's 422, or None if atomic.""" + text = _server_clean(claim) + if not text: + return "canonicalClaim is required" + n = len(text.encode("utf-16-le")) // 2 # JS String.length counts UTF-16 code units + if n > CANONICAL_CLAIM_MAX: + return f"canonicalClaim must be at most {CANONICAL_CLAIM_MAX} characters (got {n})" + if len([s for s in re.split(r"[.!?]+", text) if s.strip()]) > 1: + return "canonicalClaim must be a single sentence" + m = _CLAUSE_CONJUNCTION.search(text) + if m and _VERB_HINT.search(text[: m.start()]) and _VERB_HINT.search(text[m.end():]): + return "canonicalClaim bundles multiple propositions (top-level conjunction joins two verb-bearing clauses)" + for p in _HEDGES: + if p.search(text): + return "canonicalClaim carries a motte-and-bailey hedge" + return None + + +def lint_corpus(topics: list[dict]) -> list[str]: + """Lint every claim in a corpus; returns human-readable failures (empty = clean).""" + failures = [] + for t in topics: + err = lint_atomic_claim(t.get("canonicalClaim") or t.get("content") or "") + if err: + failures.append(f"{t['title'][:70]} → {err}") + return failures + + def seed_topic_batch(prefix: str, topics: list[dict]) -> dict[str, Optional[str]]: """Register agents and create a batch of topics round-robin. Idempotent. @@ -198,7 +297,13 @@ def seed_topic_batch(prefix: str, topics: list[dict]) -> dict[str, Optional[str] # so N agents → N topics with zero voting required. The 5-minute voting age # gate (sites/source/src/lib/auth.ts) makes the "vote on your peers" path # impractical for a one-shot seed, so we just pay the registration cost. - n_agents = len(topics) + + bad = lint_corpus(topics) + if bad: + print(f"\n=== {prefix}: {len(bad)} claim(s) fail the atomic-claim rule — nothing registered, nothing written ===") + for line in bad: + print(" ", line) + sys.exit(2) if DRY_RUN: print(f"\n=== DRY RUN — {prefix}: {len(topics)} topics, no writes ===") @@ -215,16 +320,37 @@ def seed_topic_batch(prefix: str, topics: list[dict]) -> dict[str, Optional[str] f"{would_create} agent registrations would be needed") return plan - print(f"\n=== Registering {n_agents} agents for {prefix} ===") - keys = register_agents(prefix, n_agents) - print(f" got {len(keys)} API keys") - - print(f"\n=== Creating {len(topics)} topics ({prefix}) ===") + # Existence check BEFORE any registration: a re-run after a partial apply + # must not mint agents for topics that are already on the graph. + print(f"\n=== Checking {len(topics)} titles against {BASE} ({prefix}) ===") result: dict[str, Optional[str]] = {} + for t in topics: + result[t["title"]] = find_topic_id_by_title(t["title"]) + time.sleep(0.2) + missing = [t for t in topics if not result[t["title"]]] + print(f" {len(topics) - len(missing)} already present, {len(missing)} to create") + if not missing: + print(f"\n {len(topics)}/{len(topics)} topics in place (all already existed) — nothing registered") + return result + + print(f"\n=== Registering {len(missing)} agents for {prefix} ===") + keys = register_agents(prefix, len(missing)) + print(f" got {len(keys)} API keys") + if len(keys) < len(missing): + # One agent → one topic, strictly. A key's second topic is refused by + # civic duty and fresh agents cannot vote (5-minute age gate), so + # round-robin can only produce FAILs and orphan agents. Stop here, + # before any topic is written. + print(f"FATAL: {len(keys)} keys for {len(missing)} missing topics — refusing to round-robin. " + f"Wait for the register-ip window to reset and re-run (existing topics are skipped).") + sys.exit(3) + + print(f"\n=== Creating {len(missing)} topics ({prefix}) ===") for idx, t in enumerate(topics): - # One agent → one topic. If we ever run short (e.g. registration - # failures), fall back to round-robin on whatever keys we got. - key = keys[idx] if idx < len(keys) else keys[idx % len(keys)] + if result[t["title"]]: + print(f" [{idx + 1:>2}/{len(topics)}] OK EXISTS {t['title'][:80]}") + continue + key = keys[missing.index(t)] payload = { "title": t["title"], "content": t["content"], diff --git a/scripts/pact_pow.py b/scripts/pact_pow.py index 2f9821f..0d769cb 100644 --- a/scripts/pact_pow.py +++ b/scripts/pact_pow.py @@ -50,7 +50,17 @@ def register(base: str, payload: dict[str, Any], timeout: int = 30, attempts: in url = f"{base}/api/pact/register" body = dict(payload) for _ in range(attempts): - r = requests.post(url, json=body, timeout=timeout) + r = None + for net_attempt in range(4): + try: + r = requests.post(url, json=body, timeout=timeout) + break + except requests.RequestException as e: + if net_attempt == 3: + print(f" NETWORK ERR on POST /api/pact/register: {e}") + return 0, {"error": f"network: {e}"} + time.sleep(5 * (net_attempt + 1)) + assert r is not None try: data = r.json() except ValueError: diff --git a/scripts/seed_critical_minerals.py b/scripts/seed_critical_minerals.py index cd2fdca..ebcad4c 100644 --- a/scripts/seed_critical_minerals.py +++ b/scripts/seed_critical_minerals.py @@ -9,7 +9,7 @@ No admin secret required. Idempotent. """ -from _defence_seed_helpers import seed_topic_batch # noqa: E402 +from _defence_seed_helpers import DRY_RUN, seed_topic_batch # noqa: E402 TOPICS: list[dict] = [ @@ -28,11 +28,7 @@ "jurisdiction": "AU", "authority": "Australian Government (Department of Industry, Science and Resources)", "sourceRef": "Australia's Critical Minerals Strategy 2023-2030 (June 2023); Australian Critical Minerals List 2023", - "canonicalClaim": ( - "Australia's Critical Minerals Strategy 2023-2030 names 31 critical minerals " - "(including antimony, rare earths, lithium and graphite) and allocates AUD 4bn via " - "Export Finance Australia's Critical Minerals Facility to unlock downstream processing." - ), + "canonicalClaim": "Australia's Critical Minerals Strategy 2023-2030 is accompanied by a Critical Minerals List naming 31 critical minerals", }, { "title": "US Critical Minerals List identifies 50 minerals essential to US economy and national security", @@ -48,11 +44,7 @@ "jurisdiction": "US", "authority": "US Geological Survey (Department of the Interior)", "sourceRef": "Energy Act of 2020, 30 USC 1606; US Critical Minerals List (2022)", - "canonicalClaim": ( - "The US Critical Minerals List (USGS, under 30 USC 1606) identifies 50 non-fuel minerals " - "essential to US economic and national security with disruption-vulnerable supply " - "chains, updated at least every three years." - ), + "canonicalClaim": "The USGS Critical Minerals List identifies 50 non-fuel minerals essential to US economic and national security", }, { "title": "China controls approximately 80 percent of global antimony production and refining", @@ -69,11 +61,7 @@ "jurisdiction": "INTERNATIONAL", "authority": "US Geological Survey (Mineral Commodity Summaries); China Ministry of Commerce", "sourceRef": "USGS Mineral Commodity Summaries: Antimony (2023, 2024); MOFCOM Announcement No. 33 of 2024 (antimony export licensing)", - "canonicalClaim": ( - "China supplies roughly 80 percent of global antimony (mine production and refining) " - "per USGS data, and in August 2024 announced export licensing on antimony and its " - "compounds effective 15 September 2024." - ), + "canonicalClaim": "USGS reports recent Chinese antimony mine production near 40,000 tonnes of a global total near 83,000 tonnes", }, { "title": "China controls approximately 70 percent of global rare-earth mining and over 85 percent of refining", @@ -89,11 +77,7 @@ "jurisdiction": "INTERNATIONAL", "authority": "US Geological Survey; International Energy Agency (Critical Minerals Outlook)", "sourceRef": "USGS Mineral Commodity Summaries: Rare Earths (2024); IEA Critical Minerals Outlook 2024", - "canonicalClaim": ( - "China accounts for approximately 70 percent of global rare-earth mine production and " - "over 85 percent of separation / refining capacity per USGS and IEA data, with " - "accelerating export controls on rare-earth processing technology since 2023." - ), + "canonicalClaim": "China accounts for about 70 percent of global rare-earth mine production per USGS data", }, { "title": "AUKUS Critical Minerals Cooperation coordinates trilateral supply-chain investment", @@ -109,12 +93,7 @@ "jurisdiction": "INTERNATIONAL", "authority": "AUKUS partner governments (AU, UK, US)", "sourceRef": "Australia-US Climate, Critical Minerals and Clean Energy Transformation Compact (May 2023); AUKUS Joint Leaders Statement (2023)", - "canonicalClaim": ( - "AUKUS partners have formally designated Australia as a domestic source for DPA Title " - "III purposes via the May 2023 Australia-US Climate, Critical Minerals and Clean Energy " - "Transformation Compact, coordinating trilateral critical-minerals supply-chain " - "investment." - ), + "canonicalClaim": "The 2023 Australia-US Climate, Critical Minerals and Clean Energy Compact designates Australia a domestic source under DPA Title III", }, { "title": "Quad Critical Minerals Partnership coordinates diversification across AU, IN, JP and US", @@ -130,12 +109,7 @@ "jurisdiction": "INTERNATIONAL", "authority": "Quad partner governments (Australia, India, Japan, United States)", "sourceRef": "Quad Joint Leaders Statements 2021-2024; Quad Critical and Emerging Technology Working Group", - "canonicalClaim": ( - "The Quad Critical and Emerging Technology Working Group coordinates critical-minerals " - "supply-chain diversification between Australia, India, Japan and the United States, " - "spanning reserve mapping, trade-restriction information sharing, ESG standards and " - "export-credit alignment." - ), + "canonicalClaim": "The Quad Critical and Emerging Technology Working Group coordinates critical-minerals supply-chain diversification among its members", }, { "title": "EU Critical Raw Materials Act sets 2030 domestic benchmarks for extraction, processing and recycling", @@ -151,12 +125,7 @@ "jurisdiction": "EU", "authority": "European Parliament and Council", "sourceRef": "Regulation (EU) 2024/1252 (Critical Raw Materials Act)", - "canonicalClaim": ( - "The EU Critical Raw Materials Act (Regulation 2024/1252) sets binding 2030 benchmarks " - "for EU extraction (10%), processing (40%), recycling (25%) and single-country sourcing " - "(<=65%) of strategic raw materials, with faster permitting for designated Strategic " - "Projects." - ), + "canonicalClaim": "The EU Critical Raw Materials Act (Regulation 2024/1252) sets binding 2030 benchmarks for extraction, processing and recycling", }, { "title": "Japan JOGMEC maintains strategic metals stockpiles and co-invests in overseas supply", @@ -172,11 +141,7 @@ "jurisdiction": "JP", "authority": "Japan Organization for Metals and Energy Security (under METI)", "sourceRef": "Japan Organization for Metals and Energy Security Act (Act No. 94 of 2002); METI Stockpile Policy", - "canonicalClaim": ( - "Japan's JOGMEC operates a 60-day rare-metals strategic stockpile under METI policy " - "and takes equity positions in overseas critical-minerals projects (including " - "Australian rare-earth separation) to secure off-take." - ), + "canonicalClaim": "Japan's JOGMEC operates a 60-day strategic stockpile of rare metals under METI policy", }, ] @@ -184,7 +149,11 @@ def main() -> None: result = seed_topic_batch("seed-crit-min", TOPICS) ok = sum(1 for v in result.values() if v) - print(f"\n=== seed_critical_minerals.py complete: {ok}/{len(TOPICS)} topics in place ===") + if DRY_RUN: + print(f"\n=== seed_critical_minerals.py dry run: {ok}/{len(TOPICS)} already present, " + f"{len(TOPICS) - ok} to create — nothing written ===") + else: + print(f"\n=== seed_critical_minerals.py complete: {ok}/{len(TOPICS)} topics in place ===") if __name__ == "__main__": diff --git a/scripts/seed_defence_au.py b/scripts/seed_defence_au.py index 56d80a4..750c543 100644 --- a/scripts/seed_defence_au.py +++ b/scripts/seed_defence_au.py @@ -14,7 +14,7 @@ python seed_defence_au.py # targets https://source.tailor.au SOURCE_BASE=http://localhost:3001 python seed_defence_au.py # dev """ -from _defence_seed_helpers import seed_topic_batch # noqa: E402 +from _defence_seed_helpers import DRY_RUN, seed_topic_batch # noqa: E402 TOPICS: list[dict] = [ @@ -31,11 +31,7 @@ "jurisdiction": "AU", "authority": "Commonwealth Parliament of Australia", "sourceRef": "Defence Trade Controls Act 2012 (Cth), Act No. 153 of 2012", - "canonicalClaim": ( - "The Defence Trade Controls Act 2012 (Cth) requires a permit for the supply, brokering " - "or publication of DSGL-listed defence and dual-use technology out of Australia, with " - "criminal penalties up to 10 years imprisonment." - ), + "canonicalClaim": "The Defence Trade Controls Act 2012 (Cth) requires a permit for the supply of DSGL-listed technology out of Australia", }, { "title": "Defence and Strategic Goods List (DSGL) enumerates export-controlled goods and technology", @@ -51,10 +47,7 @@ "jurisdiction": "AU", "authority": "Department of Defence (Defence Export Controls)", "sourceRef": "Defence and Strategic Goods List (current edition), made under the Customs Act 1901 (Cth) and DTCA 2012", - "canonicalClaim": ( - "The DSGL defines the goods and technology whose export from Australia requires a permit " - "under the DTCA 2012 and Customs Act 1901 prohibited-exports regime." - ), + "canonicalClaim": "The Defence and Strategic Goods List enumerates the goods and technology whose export from Australia requires a permit", }, { "title": "Defence Industry Security Program (DISP) is required for access to classified Defence contracts", @@ -69,10 +62,7 @@ "jurisdiction": "AU", "authority": "Department of Defence (Defence Industry Security Office)", "sourceRef": "Defence Industry Security Program — DISP Member Handbook (current edition)", - "canonicalClaim": ( - "DISP membership is the Australian Government's minimum security-accreditation baseline " - "for industry access to classified Defence information, assets and contracts." - ), + "canonicalClaim": "DISP membership is the minimum security accreditation for industry access to classified Defence information and contracts", }, { "title": "FIRB critical-technologies list triggers mandatory foreign-investment notification", @@ -88,11 +78,7 @@ "jurisdiction": "AU", "authority": "Foreign Investment Review Board (Treasury)", "sourceRef": "Foreign Acquisitions and Takeovers Act 1975 (Cth) s 55B; List of Critical Technologies in the National Interest (2023)", - "canonicalClaim": ( - "Acquisitions of Australian businesses operating in listed critical-technology sectors " - "(including critical minerals) trigger mandatory FIRB notification under FATA s 55B, " - "irrespective of monetary threshold." - ), + "canonicalClaim": "Acquiring an Australian business in a listed critical-technology sector requires FIRB notification under FATA s 55B regardless of value", }, { "title": "ASX Listing Rule 3.1 requires immediate disclosure of price-sensitive information", @@ -108,11 +94,7 @@ "jurisdiction": "AU", "authority": "ASX Limited (with statutory backing via Corporations Act s 674)", "sourceRef": "ASX Listing Rules Chapter 3, Rule 3.1 (continuous disclosure); Corporations Act 2001 (Cth) s 674", - "canonicalClaim": ( - "ASX Listing Rule 3.1 requires immediate disclosure to the market of information a " - "reasonable person would expect to materially affect the price or value of an entity's " - "securities, backed by Corporations Act s 674." - ), + "canonicalClaim": "ASX Listing Rules require immediate disclosure of information a reasonable person would expect to materially affect a security's price", }, { "title": "JORC Code 2012 governs public reporting of mineral exploration results, resources and reserves", @@ -128,11 +110,7 @@ "jurisdiction": "AU", "authority": "Joint Ore Reserves Committee (AusIMM, AIG, MCA)", "sourceRef": "JORC Code 2012 (Australasian Code for Reporting of Exploration Results, Mineral Resources and Ore Reserves)", - "canonicalClaim": ( - "JORC Code 2012 mandates Competent Person sign-off and Inferred/Indicated/Measured " - "classification for public resource statements by ASX-listed mining companies, via ASX " - "Listing Rule 5.6." - ), + "canonicalClaim": "JORC Code 2012 mandates Competent Person sign-off for public mineral resource statements by ASX-listed companies", }, { "title": "Customs Act 1901 (Cth) prohibited-exports regime criminalises unauthorised export of DSGL goods", @@ -147,11 +125,7 @@ "jurisdiction": "AU", "authority": "Commonwealth Parliament of Australia; Australian Border Force", "sourceRef": "Customs Act 1901 (Cth) s 112; Customs (Prohibited Exports) Regulations 1958", - "canonicalClaim": ( - "The Customs Act 1901 s 112 and Prohibited Exports Regulations 1958 criminalise " - "unauthorised export of DSGL-listed goods from Australia with penalties up to 10 years " - "imprisonment." - ), + "canonicalClaim": "Customs Act 1901 (Cth) s 112 criminalises the unauthorised export of DSGL-listed goods from Australia", }, { "title": "AUKUS Pillar 2 establishes trilateral advanced-capability technology transfer between AU, UK and US", @@ -167,11 +141,7 @@ "jurisdiction": "AU", "authority": "Department of Defence; trilateral AUKUS working arrangements", "sourceRef": "AUKUS Pillar 2 Joint Leaders Statement (2021); Defence Trade Controls Amendment Act 2024 (Cth); 22 CFR 126.7 AUKUS exemption", - "canonicalClaim": ( - "AUKUS Pillar 2 is the trilateral framework for advanced-capability technology transfer " - "between Australia, the UK and the US, operationalised via reciprocal national export-" - "control exemptions." - ), + "canonicalClaim": "AUKUS Pillar 2 is the trilateral framework for advanced-capability technology transfer between Australia, the UK and the US", }, { "title": "Safeguards Act 1987 (Cth) implements Australia's nuclear non-proliferation obligations", @@ -186,10 +156,7 @@ "jurisdiction": "AU", "authority": "Australian Safeguards and Non-Proliferation Office (DFAT)", "sourceRef": "Nuclear Non-Proliferation (Safeguards) Act 1987 (Cth), Act No. 8 of 1987", - "canonicalClaim": ( - "The Safeguards Act 1987 (Cth) implements Australia's NPT and IAEA obligations and " - "controls possession, transport and communication of nuclear material and technology." - ), + "canonicalClaim": "The Safeguards Act 1987 (Cth) implements Australia's obligations under the Nuclear Non-Proliferation Treaty", }, { "title": "Weapons of Mass Destruction (Prevention of Proliferation) Act 1995 (Cth) prohibits WMD programme support", @@ -204,11 +171,7 @@ "jurisdiction": "AU", "authority": "Commonwealth Parliament of Australia; Department of Foreign Affairs and Trade", "sourceRef": "Weapons of Mass Destruction (Prevention of Proliferation) Act 1995 (Cth), Act No. 72 of 1995", - "canonicalClaim": ( - "The WMD Act 1995 (Cth) applies a catch-all prohibition on supplying goods, services or " - "technology where the supplier knows or ought to suspect the end-use involves a WMD " - "programme, beyond the listed-goods DSGL regime." - ), + "canonicalClaim": "The WMD (Prevention of Proliferation) Act 1995 (Cth) prohibits supplying goods or services suspected to serve a WMD programme", }, { "title": "Autonomous Sanctions Act 2011 (Cth) authorises Australian targeted financial and trade sanctions", @@ -224,10 +187,7 @@ "jurisdiction": "AU", "authority": "Department of Foreign Affairs and Trade (Australian Sanctions Office)", "sourceRef": "Autonomous Sanctions Act 2011 (Cth); Autonomous Sanctions Regulations 2011; 2021 Magnitsky amendments", - "canonicalClaim": ( - "The Autonomous Sanctions Act 2011 (Cth) authorises Australia's targeted financial, " - "travel and sectoral sanctions, extended in 2021 to thematic Magnitsky-style designations." - ), + "canonicalClaim": "The Autonomous Sanctions Act 2011 (Cth) authorises Australia's targeted financial, travel and sectoral sanctions", }, { "title": "National Security Legislation Amendment (Espionage and Foreign Interference) Act 2018 criminalises foreign interference", @@ -242,11 +202,7 @@ "jurisdiction": "AU", "authority": "Commonwealth Parliament of Australia", "sourceRef": "National Security Legislation Amendment (Espionage and Foreign Interference) Act 2018 (Cth); Criminal Code 1995 (Cth) Division 92", - "canonicalClaim": ( - "The NSLA EFI Act 2018 inserted modern espionage, foreign-interference and sabotage " - "offences into the Commonwealth Criminal Code and complements the Foreign Influence " - "Transparency Scheme." - ), + "canonicalClaim": "The NSLA (Espionage and Foreign Interference) Act 2018 (Cth) inserted foreign-interference offences into the Criminal Code 1995", }, ] @@ -254,7 +210,11 @@ def main() -> None: result = seed_topic_batch("seed-au-def", TOPICS) ok = sum(1 for v in result.values() if v) - print(f"\n=== seed_defence_au.py complete: {ok}/{len(TOPICS)} topics in place ===") + if DRY_RUN: + print(f"\n=== seed_defence_au.py dry run: {ok}/{len(TOPICS)} already present, " + f"{len(TOPICS) - ok} to create — nothing written ===") + else: + print(f"\n=== seed_defence_au.py complete: {ok}/{len(TOPICS)} topics in place ===") if __name__ == "__main__": diff --git a/scripts/seed_defence_us.py b/scripts/seed_defence_us.py index 26ef32c..e397736 100644 --- a/scripts/seed_defence_us.py +++ b/scripts/seed_defence_us.py @@ -9,7 +9,7 @@ No admin secret required. Idempotent. """ -from _defence_seed_helpers import seed_topic_batch # noqa: E402 +from _defence_seed_helpers import DRY_RUN, seed_topic_batch # noqa: E402 TOPICS: list[dict] = [ @@ -29,11 +29,7 @@ "jurisdiction": "US", "authority": "US Department of State (Directorate of Defense Trade Controls)", "sourceRef": "International Traffic in Arms Regulations, 22 CFR 120-130; Arms Export Control Act, 22 USC 2778", - "canonicalClaim": ( - "ITAR (22 CFR 120-130) controls the export, re-export and brokering of US Munitions " - "List articles, services, and technical data — including deemed exports to foreign " - "persons inside the US — with criminal penalties up to 20 years imprisonment." - ), + "canonicalClaim": "ITAR (22 CFR 120-130) controls the export of US Munitions List defence articles, services and technical data", }, { "title": "EAR (15 CFR 730-774) governs the export of US dual-use and commercial technology", @@ -49,11 +45,7 @@ "jurisdiction": "US", "authority": "US Department of Commerce (Bureau of Industry and Security)", "sourceRef": "Export Administration Regulations, 15 CFR 730-774; Export Control Reform Act of 2018, 50 USC 4801", - "canonicalClaim": ( - "EAR (15 CFR 730-774) controls US commercial and dual-use exports via ECCN classification " - "on the Commerce Control List, with licence determinations driven by destination, " - "end-user and end-use analysis administered by BIS." - ), + "canonicalClaim": "EAR (15 CFR 730-774) controls US dual-use exports through ECCN classification on the Commerce Control List", }, { "title": "NEPA (42 USC 4321+) requires environmental impact assessment for major federal actions", @@ -70,11 +62,7 @@ "jurisdiction": "US", "authority": "US Council on Environmental Quality; federal action agencies", "sourceRef": "National Environmental Policy Act of 1969, 42 USC 4321 et seq; 40 CFR Parts 1500-1508", - "canonicalClaim": ( - "NEPA (42 USC 4321+) requires federal agencies to prepare an Environmental Impact " - "Statement for major federal actions significantly affecting the human environment, " - "implemented via 40 CFR 1500-1508." - ), + "canonicalClaim": "NEPA (42 USC 4321 et seq) requires an Environmental Impact Statement for major federal actions significantly affecting the human environment", }, { "title": "BLM 43 CFR 3809 governs surface management of hardrock mining on federal land", @@ -90,10 +78,7 @@ "jurisdiction": "US", "authority": "US Bureau of Land Management (Department of the Interior)", "sourceRef": "43 CFR Part 3809, Subparts 3809.1-3809.605 (Surface Management)", - "canonicalClaim": ( - "BLM 43 CFR 3809 regulates hardrock mining on federal land via the Notice / Plan-of-" - "Operations regime, triggering NEPA review, reclamation bonding and Tribal consultation." - ), + "canonicalClaim": "BLM 43 CFR 3809 requires an approved Plan of Operations for commercial hardrock mining on federal land", }, { "title": "DFARS 252.225-7052 restricts DoD acquisition of non-domestic specialty metals including antimony", @@ -110,11 +95,7 @@ "jurisdiction": "US", "authority": "US Department of Defense (Defense Acquisition Regulations System)", "sourceRef": "DFARS 252.225-7052; DFARS 252.225-7008; 10 USC 4863 (specialty metals)", - "canonicalClaim": ( - "DFARS 252.225-7052 and the underlying specialty-metals statute (10 USC 4863) restrict " - "DoD acquisition of end items containing non-domestic specialty metals including " - "antimony, tungsten, samarium-cobalt and NdFeB magnets." - ), + "canonicalClaim": "The DFARS specialty-metals clause bars DoD end items containing specialty metals not melted or produced in the US or a qualifying country", }, { "title": "California SMARA requires state permitting and reclamation for surface-mining operations", @@ -129,11 +110,7 @@ "jurisdiction": "US-CA", "authority": "California Department of Conservation (Division of Mine Reclamation); State Mining and Geology Board", "sourceRef": "Surface Mining and Reclamation Act of 1975, California Public Resources Code §§ 2710-2796", - "canonicalClaim": ( - "California SMARA (PRC §§ 2710-2796) requires surface-mining operators in California to " - "obtain a local lead-agency permit, approved Reclamation Plan and financial assurance, " - "overseen by the State Geologist and State Mining and Geology Board." - ), + "canonicalClaim": "California SMARA requires an approved reclamation plan and financial assurance for surface-mining operations", }, { "title": "Defense Production Act Title III funds domestic industrial-base expansion for critical materials", @@ -149,11 +126,7 @@ "jurisdiction": "US", "authority": "Executive Office of the President; Department of Defense (Office of the Under Secretary for Acquisition and Sustainment)", "sourceRef": "Defense Production Act of 1950, Title III, 50 USC 4531-4533", - "canonicalClaim": ( - "DPA Title III (50 USC 4531-4533) authorises the President to fund expansion of US " - "domestic industrial capacity for defence-essential materials through purchase " - "commitments, guarantees and plant-expansion grants." - ), + "canonicalClaim": "DPA Title III (50 USC 4531-4533) authorises federal funding to expand domestic industrial capacity for defence-essential materials", }, { "title": "CFIUS reviews foreign investment in US critical-technology and critical-minerals businesses", @@ -169,12 +142,7 @@ "jurisdiction": "US", "authority": "US Department of the Treasury (chair of CFIUS); interagency committee", "sourceRef": "Section 721 of the Defense Production Act, 50 USC 4565; Foreign Investment Risk Review Modernization Act 2018", - "canonicalClaim": ( - "CFIUS, reformed by FIRRMA 2018, reviews foreign acquisitions of US businesses in " - "critical technology, critical infrastructure, sensitive personal data and covered " - "real-estate near national-security sites, with mandatory declarations for specified " - "transactions." - ), + "canonicalClaim": "CFIUS reviews foreign acquisitions of US businesses in critical technology, critical infrastructure or sensitive personal data", }, { "title": "Inflation Reduction Act ties clean-energy tax credits to critical-minerals sourcing requirements", @@ -191,12 +159,7 @@ "jurisdiction": "US", "authority": "US Congress; Internal Revenue Service (Treasury)", "sourceRef": "Inflation Reduction Act of 2022, Pub L 117-169; Internal Revenue Code §§ 30D, 45X, 45, 45Y", - "canonicalClaim": ( - "The Inflation Reduction Act conditions the IRC § 30D clean-vehicle credit on " - "critical-minerals content sourced from the US or a US free-trade-agreement partner " - "(explicitly including Australia) and excludes content sourced from Foreign Entities " - "of Concern." - ), + "canonicalClaim": "The IRA conditions the IRC s 30D clean-vehicle credit on critical minerals sourced from the US or a free-trade-agreement partner", }, { "title": "Buy American Act 1933 requires federal agencies to prefer domestic end products in procurement", @@ -211,11 +174,7 @@ "jurisdiction": "US", "authority": "US Congress; federal contracting agencies", "sourceRef": "Buy American Act, 41 USC 8301-8305; FAR Part 25; 2022 Executive Order 14005 domestic-content amendments", - "canonicalClaim": ( - "The Buy American Act 1933 (41 USC 8301-8305) requires federal agencies to prefer " - "domestic end products in procurement, with the domestic-content threshold rising to " - "75% by 2029 under the 2022 amendments." - ), + "canonicalClaim": "The Buy American Act 1933 (41 USC 8301-8305) requires federal agencies to prefer domestic end products in procurement", }, ] @@ -223,7 +182,11 @@ def main() -> None: result = seed_topic_batch("seed-us-def", TOPICS) ok = sum(1 for v in result.values() if v) - print(f"\n=== seed_defence_us.py complete: {ok}/{len(TOPICS)} topics in place ===") + if DRY_RUN: + print(f"\n=== seed_defence_us.py dry run: {ok}/{len(TOPICS)} already present, " + f"{len(TOPICS) - ok} to create — nothing written ===") + else: + print(f"\n=== seed_defence_us.py complete: {ok}/{len(TOPICS)} topics in place ===") if __name__ == "__main__": diff --git a/scripts/seed_topic_dependencies.py b/scripts/seed_topic_dependencies.py index fa5a13e..ea890d6 100644 --- a/scripts/seed_topic_dependencies.py +++ b/scripts/seed_topic_dependencies.py @@ -19,6 +19,7 @@ api, find_topic_id_by_title, register_agents, + reusable_key, ) @@ -274,14 +275,22 @@ def main() -> None: ) return - keys = register_agents("seed-deps", 1) - agent_key = keys[0] - edges, missing = resolve_edges() if not edges: print("\nFATAL: no edges resolved. Did the three topic seed scripts run first?") sys.exit(1) + # Reuse a topic-script key when one is available (PACT_SEED_AGENT_KEY or the last + # line of PACT_SEED_KEYS_FILE): edge writes need any registered agent, and the + # per-address registration budget is spent by the 30 topic agents. Otherwise + # register only once there is something to write. + agent_key = reusable_key() + if agent_key: + print(" reusing a topic-script agent key for the edge writes (no registration)") + else: + keys = register_agents("seed-deps", 1) + agent_key = keys[0] + print(f"\n=== Creating {len(edges)} dependency edges ===") created = 0 existed = 0