diff --git a/.gitattributes b/.gitattributes
index d24fdf4c3316a38a23819b13d2524ea4f22214fd..ba46f90b91ca5d8c5ac6c4af8fb2583a000cf10f 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -45,3 +45,23 @@ root/specialists/cooking/nutrition_v1.sqlite3 filter=lfs diff=lfs merge=lfs -tex
root/specialists/warehouse-r2/site_fresh.sqlite3 filter=lfs diff=lfs merge=lfs -text
root/specialists/warehouse/site_fresh.sqlite3 filter=lfs diff=lfs merge=lfs -text
root/fable-content/oracle-m-series-2026-08-15/m30_admitted_closure_v1.sqlite3 filter=lfs diff=lfs merge=lfs -text
+root/specialists/_proved/bin/acoustics filter=lfs diff=lfs merge=lfs -text
+root/specialists/_proved/bin/anatomyagri filter=lfs diff=lfs merge=lfs -text
+root/specialists/_proved/bin/chemistry filter=lfs diff=lfs merge=lfs -text
+root/specialists/_proved/bin/climate filter=lfs diff=lfs merge=lfs -text
+root/specialists/_proved/bin/computing filter=lfs diff=lfs merge=lfs -text
+root/specialists/_proved/bin/decide filter=lfs diff=lfs merge=lfs -text
+root/specialists/_proved/bin/discrete filter=lfs diff=lfs merge=lfs -text
+root/specialists/_proved/bin/emoptics filter=lfs diff=lfs merge=lfs -text
+root/specialists/_proved/bin/energy filter=lfs diff=lfs merge=lfs -text
+root/specialists/_proved/bin/facts filter=lfs diff=lfs merge=lfs -text
+root/specialists/_proved/bin/fusion filter=lfs diff=lfs merge=lfs -text
+root/specialists/_proved/bin/geostats filter=lfs diff=lfs merge=lfs -text
+root/specialists/_proved/bin/humanities filter=lfs diff=lfs merge=lfs -text
+root/specialists/_proved/bin/life filter=lfs diff=lfs merge=lfs -text
+root/specialists/_proved/bin/materials filter=lfs diff=lfs merge=lfs -text
+root/specialists/_proved/bin/mechanics filter=lfs diff=lfs merge=lfs -text
+root/specialists/_proved/bin/quantity filter=lfs diff=lfs merge=lfs -text
+root/specialists/_proved/bin/space filter=lfs diff=lfs merge=lfs -text
+root/specialists/_proved/bin/stores filter=lfs diff=lfs merge=lfs -text
+root/specialists/_proved/bin/trades filter=lfs diff=lfs merge=lfs -text
diff --git a/README.md b/README.md
index 5dd48d07fa287ded229d68164797bcd61f9f2502..57092402ff8dbc486427e5e6b85bc2551f435cc6 100644
--- a/README.md
+++ b/README.md
@@ -91,6 +91,8 @@ It listens on 127.0.0.1 only and sends nothing anywhere except to services you c
## Verify this download
+**Release 4.0 (20 October 2026).** Every answer's route is now spelled as a word over the 22 paths of the three-tree runtime, and two numbers are kept for it: k, the number of steps, and L, the time it took. A question that did not form an answer is written to a hash-chained residue ledger, typed by the node where it stopped. An answer with nothing under it (no citation, proof or checked route) no longer becomes conversation state. Walks already made are recalled instead of walked again. Pronouns: when the chain cannot resolve a follow-up's pronoun, the previous cited answer is put in its place and the question is asked once more, and the reply says so ("Taking 'its' as Japan, from my previous answer."). This repository did not receive release 3.2 when the slim one did; 4.0 carries it (proved specialists as a cross-check, four star specialists, owner-first; see the slim repository's 3.2 note). The full 3.2 missed two sealed tests: sealed test 23 (three false yeses against a bar of two) and sealed test 24 (one format defect: a speed asked in km/h answered in m/h). Three guards were added after sealed test 23 (w556): owner-first may not return a stored fact for a question that gives no numbers; a negative amount of something that cannot be negative is withheld; arithmetic is not read from a sentence. A fourth guard (evidence must name what the question names) lost right answers on every form measured and is not included; the two sealed-23 errors it was aimed at stay open. Release test, PI4-B (186 turns, sealed before the code froze, run once): 157 right, 26 declined, 3 of 30 should-decline questions answered (false yeses); every answer to an answerable question was right. The same panel on the full 3.2 runtime: 157 right, 3 false yeses. Second turns of follow-ups: 23 right, 0 wrong. Mean steps k 4.946; median time L 120.35 ms. Route forms re-entered on recurring questions: 94.0% (shuffled baseline 16.5%); recurring pairs spelled the same word 20/20. Disclosed: pronoun re-entry fired on none of PI4-B's follow-ups (the 3.2 runtime already resolves them); on the first panel, PI4-A, the 4.0 and 3.2 runtimes gave identical answers; recalling walks cut mean time from 637 ms to 530 ms on 162 questions with the same k (5.636) and the same route word on all 162, so speed and step count are separate measures; four induction passes proposed no new rules; a panel generator bug (follow-up pairs not sharing a session) was fixed in the runner before PI4-B ran. New root `350cd86e190f5e451faa78e7d12f6d923bcc7cdcf46a556c70c8ac8211f5e4d4`, anchored on Solana: [4vJ57qaJxvzZ...](https://explorer.solana.com/tx/4vJ57qaJxvzZteEm3vL9sAsmsRmeeLg3X8ESk61zN29ZpGwqsCz3xcGMhoqWXy8TyqEQH9HT7Tn4c2FxoiGuvosH).
+
**Release 3.1 (17 October 2026).** Fold composition: the specialist network now learns operations from the specialists' own development exams instead of having every one written by hand (84 learned operations, each kept only when its siblings cross-checked it, each firing only when the question carries its template's names and materials), and chains them with the hand-written ones under the same checks; a learned operation never takes a value that arithmetic built to fit its slot. Found by the research record's re-examination of every specialist: when a check stops a route that could not have computed the answer (a document page asked for a derivative), the specialist that can is now asked; answers about a different quantity than the one asked (a frequency for a wavelength, a travel time for a share of sunlight), definitions given for a calculation, a first derivative given for a second, and lower-cased units are withheld or corrected; an answer in a unit of the asked dimension is restated in the asked unit. The Workbench's worked example is now a habitable-zone specialist built from one published paper. Release gate, sealed test 22 (150 questions written after the code froze, run once): 124 right, 3 wrong, 23 declined, 1 false yes; precision 97.9%. New root `c4c1edb342b54fe5faf7fdf775d27bf72d351ea0481d246c648a2e92b5269050`, anchored on Solana: [4kW4S5V4kHsF...](https://explorer.solana.com/tx/4kW4S5V4kHsFKGcxWBn37jPD73nGP17aF8eF9yPv8dEkwzTbWksvYaej2VkiUPMQ9PVdN92mmve4tAqgsr6RZ7s3). Code changed only (the fold chain, specialists/_gate and the Workbench); no store changed.
**Release patch 3 (16 October 2026).** The specialist network v5 reads the shapes the patch 2 retest (composition test C2) found unread: a box given as "5 cm by 4 cm by 3 cm" or a cube "5 cm on each side" (its volume derived by core arithmetic and shown in the path), "500 cubic centimetres of ice", "the water in the pond", "in a day". The older v2 network route is now checked like the others, and a unit answer about a place with no record ("minutes in a day on the planet Zentar") is withheld. New in the Workbench: an Essay tab and `python oracle7.py essay topic` (ProseFold) compose an essay from this model's own records, with a choice of length, reading level (measured as a grade), technical depth and style (profiles measured from Simple and English Wikipedia and the OpenStax textbooks); every sentence is kept word for word with its source, no language model, and their quality has not been measured by people yet. Release gate, sealed test 21 (150 questions written after the code froze, run once): 131 right, 2 wrong, 17 declined, 0 false yes; precision 98.1%. New root `0575c9b78f6dcc0a48ee57e6dc63d3e1a46afc13d2c9d9d4503db6c784564ed2`, anchored on Solana: [WNEdPhKJZYCh...](https://explorer.solana.com/tx/WNEdPhKJZYCh3ZKe7X8zhVehuyskEnKuDbde86W1XdKdBC9izVAJTzbrewtSUQ8keQvT1rZUBumCGnGxjDXyVgU). Code changed only (the fold chain, specialists/_gate and the Workbench); no store changed.
diff --git a/RELEASE_HASHES.json b/RELEASE_HASHES.json
index ba16c240fb5dfba5e83782c19ad1daf8488c22bb..3c4cd80b2a67596f2e01c70385514d0865e4853a 100644
--- a/RELEASE_HASHES.json
+++ b/RELEASE_HASHES.json
@@ -1,16 +1,20 @@
{
"format": "oracle7-release-hashes/1",
"release": "oracle7",
- "head": "oracle_release_runtime_w548",
- "merkle_root": "c4c1edb342b54fe5faf7fdf775d27bf72d351ea0481d246c648a2e92b5269050",
+ "head": "oracle_release_runtime_w558",
+ "merkle_root": "350cd86e190f5e451faa78e7d12f6d923bcc7cdcf46a556c70c8ac8211f5e4d4",
"parts": {
"specialists/_cards": {
"sha256": "619ae757757de886c389cf953924f090d0f71ecff02447171381287f9553a2cf",
"files": 142
},
"specialists/_gate": {
- "sha256": "958a79ab626cb7b826b8e3eb9e6decd0ca5491bc3120cf64a3b6f9f774b3ce2a",
- "files": 20
+ "sha256": "aba52f21aeffe5ed8829cfc0294dbb3e88a09131d040599d312f011e2b4bf82e",
+ "files": 23
+ },
+ "specialists/_proved": {
+ "sha256": "4ef644bab853b5f3f7883862aad58c55ca0682ba97b12f95664887d795ee6df9",
+ "files": 23
},
"specialists/acoustics": {
"sha256": "b1f834fac69da0bcb7fb3a0463559d622e1e842457d2f36aa2a821ce69dff84d",
@@ -144,6 +148,10 @@
"sha256": "11ba66e2158cf17ea81814f8365350a73ee5fa531a2e61d094c93c1dc8e9b650",
"files": 8
},
+ "specialists/decay-r2": {
+ "sha256": "8ce57db1b27f7f4f03aa288a6936e0c15d29e6d22805422f2da076fab20345ba",
+ "files": 10
+ },
"specialists/densities": {
"sha256": "d694151f731a175b784d756e84996ae32151c50314e49e46e30a72a5c1a211dc",
"files": 8
@@ -192,6 +200,10 @@
"sha256": "ae8ee5a60711c2a49ec2f0ebf9baf119e0f1511e14cf530b2c8b6cd0d8ea7837",
"files": 10
},
+ "specialists/exoorbits": {
+ "sha256": "63b6596341c7d07280829f1311adb8724fa66b9d63633b0a8394fa0c3efe26ca",
+ "files": 8
+ },
"specialists/finance": {
"sha256": "ab5cb2ac863d2a679816e4c68b54dc55cce783bbc52b83069938397166c888c6",
"files": 7
@@ -284,6 +296,10 @@
"sha256": "f49eeef1a0310637ebd9320e55897f2cbd987577a9c76d99c4b4112f62ca73bc",
"files": 7
},
+ "specialists/habzone": {
+ "sha256": "8594d61f3df4c88fba4fedf42b9a2a92b2e5b674ec4b1fb8e7bf9173ff7f9493",
+ "files": 8
+ },
"specialists/hvac": {
"sha256": "7b98d4af07c2ae6ac5f31db2b275c48c891db332150e323ea374bf668e16e464",
"files": 7
@@ -1164,6 +1180,10 @@
"sha256": "ff5aa630476ab4c3523737c54952b94b2007c943fe7b90648107a8e162aee1bd",
"files": 6
},
+ "specialists/stars": {
+ "sha256": "8821d0ab890365a15043d4bce3afa028f98848623445080168281001322e46d9",
+ "files": 10
+ },
"specialists/statistics": {
"sha256": "5448df943eaa687ec381deca697e3907d1910ceaa770cc30a8c02fb72d8e0913",
"files": 7
@@ -1180,6 +1200,10 @@
"sha256": "b92ad90fb4bca6d3e8eee4a36c3bb3d3499ab1c2ef84061f4783029f966a6602",
"files": 6
},
+ "specialists/transits": {
+ "sha256": "50f36df2bf3a8dafd419a8e2324fb95a89d0920deadbdf59e52ce0d98070001a",
+ "files": 8
+ },
"specialists/units": {
"sha256": "20140ff7585313840468cdf3eca54355fab59d5c1313ae14ec37b21f8ae9fad4",
"files": 7
@@ -1233,12 +1257,12 @@
"files": 9
},
"core/admitted": {
- "sha256": "7771dbeabd010f8bc664ff367dc6b51015a8e3b20870fd1790f04f3a46098585",
- "files": 341
+ "sha256": "5229e122a10bab42c0184826d29e0787debfb4b48a3c3100fe95ad0f0b4f03dd",
+ "files": 349
},
"workbench": {
- "sha256": "c9c21bebfb4ecf34fa0b598eb411b2a20d735f1b0e6277381fbf018f4b38c75f",
- "files": 150
+ "sha256": "d43f5a9d1607da526e3dd89c20ea2e51f98bfc950c496680d85f207592163e70",
+ "files": 151
},
"oracle7.py": {
"sha256": "9d68865540b23122e0020c0b8ee52471a71a3dfa8e3572b23b961ea4b3bf920e",
@@ -1253,15 +1277,15 @@
"anchor": {
"chain": "solana-mainnet",
"program": "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr",
- "memo": "Oracle7 release w548 3.1, 2026-10-17. oracle7 merkle c4c1edb342b54fe5faf7fdf775d27bf72d351ea0481d246c648a2e92b5269050. oracle7-slim merkle 732358ed179adc00f0404dc7ecb1fe7e11484699559f6c772a1c578b002288da. Recipe: RELEASE_HASHES.json in each repository.",
- "signature": "4kW4S5V4kHsFKGcxWBn37jPD73nGP17aF8eF9yPv8dEkwzTbWksvYaej2VkiUPMQ9PVdN92mmve4tAqgsr6RZ7s3",
- "block_time_utc": "2026-10-18T02:51:28Z",
- "slot": 458275075,
- "explorer": "https://explorer.solana.com/tx/4kW4S5V4kHsFKGcxWBn37jPD73nGP17aF8eF9yPv8dEkwzTbWksvYaej2VkiUPMQ9PVdN92mmve4tAqgsr6RZ7s3"
+ "memo": "Oracle7 release 4.0 (w558), full and slim. oracle7 merkle 350cd86e190f5e451faa78e7d12f6d923bcc7cdcf46a556c70c8ac8211f5e4d4. oracle7-slim merkle d72ed426cffd99861bec539e0776127351f4d7955d20e73583da5b931a2b55a5. Release test PI4-B, run once per variant. Recipe: RELEASE_HASHES.json in each repository.",
+ "signature": "4vJ57qaJxvzZteEm3vL9sAsmsRmeeLg3X8ESk61zN29ZpGwqsCz3xcGMhoqWXy8TyqEQH9HT7Tn4c2FxoiGuvosH",
+ "block_time_utc": "2026-10-20T00:57:16Z",
+ "slot": 459039726,
+ "explorer": "https://explorer.solana.com/tx/4vJ57qaJxvzZteEm3vL9sAsmsRmeeLg3X8ESk61zN29ZpGwqsCz3xcGMhoqWXy8TyqEQH9HT7Tn4c2FxoiGuvosH"
},
"supersedes": {
- "merkle_root": "0575c9b78f6dcc0a48ee57e6dc63d3e1a46afc13d2c9d9d4503db6c784564ed2",
- "anchor_signature": "WNEdPhKJZYCh3ZKe7X8zhVehuyskEnKuDbde86W1XdKdBC9izVAJTzbrewtSUQ8keQvT1rZUBumCGnGxjDXyVgU",
- "reason": "release 3.1 (17 October 2026): fold composition - specialist network v6 composes operations learned from the specialists' own exams (w549); the specialist answers after a check stops a route that could not know (w548); 3.1 correctness rules from the research record's re-examination and composition rounds (w547); the Workbench's worked example is the habitable-zone specialist; release gate: sealed test 22."
+ "merkle_root": "c4c1edb342b54fe5faf7fdf775d27bf72d351ea0481d246c648a2e92b5269050",
+ "anchor_signature": "4kW4S5V4kHsFKGcxWBn37jPD73nGP17aF8eF9yPv8dEkwzTbWksvYaej2VkiUPMQ9PVdN92mmve4tAqgsr6RZ7s3",
+ "reason": "release 4.0 (2026): the law, built from an invocation Ben wrote (w557): every answer's path spelled over the twenty-two paths of the three-tree runtime and counted before release; a hash-chained residue ledger of what did not form, typed by where on the Tree it stopped; an answer with nothing under it does not become conversation state; walks recalled, not re-walked; a pronoun the chain cannot resolve re-enters the previous cited answer, said aloud (w558). On the full 3.2 head (w556: owner-first needs givens; negative amounts withheld; arithmetic is not read from a sentence). Release test: PI4-B."
}
}
\ No newline at end of file
diff --git a/RELEASE_HASHES_before_40.json b/RELEASE_HASHES_before_40.json
new file mode 100644
index 0000000000000000000000000000000000000000..ba16c240fb5dfba5e83782c19ad1daf8488c22bb
--- /dev/null
+++ b/RELEASE_HASHES_before_40.json
@@ -0,0 +1,1267 @@
+{
+ "format": "oracle7-release-hashes/1",
+ "release": "oracle7",
+ "head": "oracle_release_runtime_w548",
+ "merkle_root": "c4c1edb342b54fe5faf7fdf775d27bf72d351ea0481d246c648a2e92b5269050",
+ "parts": {
+ "specialists/_cards": {
+ "sha256": "619ae757757de886c389cf953924f090d0f71ecff02447171381287f9553a2cf",
+ "files": 142
+ },
+ "specialists/_gate": {
+ "sha256": "958a79ab626cb7b826b8e3eb9e6decd0ca5491bc3120cf64a3b6f9f774b3ce2a",
+ "files": 20
+ },
+ "specialists/acoustics": {
+ "sha256": "b1f834fac69da0bcb7fb3a0463559d622e1e842457d2f36aa2a821ce69dff84d",
+ "files": 6
+ },
+ "specialists/additive-v2": {
+ "sha256": "56bf6cdba671418df144e7caedfab6549ef1d436b3065d0c1d6f244c3ba4d9ab",
+ "files": 9
+ },
+ "specialists/agriculture-v2": {
+ "sha256": "523033353b845b037316bb4b71250441ec4ff6ba398ba3133dea5297c43680d8",
+ "files": 11
+ },
+ "specialists/airquality": {
+ "sha256": "befc3811a2e40c142a378d3f058a97dcec6abb7265d22d875de0d39884051ccf",
+ "files": 8
+ },
+ "specialists/algorithms": {
+ "sha256": "ad309ce3c7881029bfe872d7de0942a136c6329822733c9489d4ea6bea44f78c",
+ "files": 7
+ },
+ "specialists/anatomy": {
+ "sha256": "a21bd232538cb1323fae467a05fefcdc717621ce4c4174a57ceb58dd0152d32f",
+ "files": 8
+ },
+ "specialists/anatomy-r2": {
+ "sha256": "3eb6374536765ee657f4e17c5cc9ef10ef7a7846746c79c4a4e2d4c5e254c30f",
+ "files": 7
+ },
+ "specialists/architecture": {
+ "sha256": "58dfad2ed6b36f50a0c4c755090ded6f01f7476eeac689eaf488a8aab9de5164",
+ "files": 8
+ },
+ "specialists/arthistory": {
+ "sha256": "696412350d6532270d324236a3e3aec58a27bc21ec9c359988c5dee3ab1f2d16",
+ "files": 10
+ },
+ "specialists/astronomy": {
+ "sha256": "58408c64c5d3c3cb6df1124e3db87fd74cfa6bad0d8cdcab2ebf11657f0fbe75",
+ "files": 8
+ },
+ "specialists/astronomy-v2": {
+ "sha256": "c0011119e9775b93859ff012fa6a8b1fdd1297a281c8f8411802370fb88163bb",
+ "files": 7
+ },
+ "specialists/astronomy-v3": {
+ "sha256": "bd211531997204300cdb08d777da9e5aac96d56120a2e919334919baecd2f32d",
+ "files": 8
+ },
+ "specialists/astronomy-v3-r2": {
+ "sha256": "3c406f686faba66a057f5cd9eb87e8bc068e7f7f746e2cd4d6ab3de3ca072766",
+ "files": 14
+ },
+ "specialists/biology": {
+ "sha256": "7d23c6ce45e5ddf686bd1ed34d3c8a8be9a37e29682c73b10c42bbc07181b6aa",
+ "files": 8
+ },
+ "specialists/bodies-v2": {
+ "sha256": "aca39dcb39cb470c8bbf9995571fc869f5fb28431e2acad9af4143175508f053",
+ "files": 9
+ },
+ "specialists/botany": {
+ "sha256": "5af91f59a42663c0befc53099a6aa62e8bddc684abf3a1134a6e95dd641e6766",
+ "files": 10
+ },
+ "specialists/calculus": {
+ "sha256": "6b9fef0d96329117eb10220ab5eddb33fb1534da455c2d5db18db1eaa5a114e5",
+ "files": 6
+ },
+ "specialists/carboncapture": {
+ "sha256": "99e32723e5c04e1d3cfa4b515a642d9e480dae4978e55ac01652721477977e8b",
+ "files": 8
+ },
+ "specialists/carbonmat": {
+ "sha256": "64f1f87981f10f39fcef287a85ab7d578dfbfe54a7e185dc79aa918a83030a89",
+ "files": 8
+ },
+ "specialists/chemistry": {
+ "sha256": "21ebebe15bb58bb6e0a230b2e2fbe4e77f373fbde762ac1779c91fb945c2e281",
+ "files": 8
+ },
+ "specialists/chemistry-v3": {
+ "sha256": "78495138a3ee05df30c872bc095b36813b4c99c7de320521aa2fbee0e947225b",
+ "files": 12
+ },
+ "specialists/childdev": {
+ "sha256": "2e706a0ab4be8a1185d7a8dcd6848ed1ef84438f661dacd8eaf3438f7e71d72a",
+ "files": 10
+ },
+ "specialists/climate": {
+ "sha256": "f42048beb6bab4e312d0db1d79978eef5907b3274ee24108dad65bbf8aa5dfb2",
+ "files": 8
+ },
+ "specialists/coldchain": {
+ "sha256": "e150790aef318f2e8f0362c701e8bb066aaae4718de5491144d2e9e50e6ebe01",
+ "files": 7
+ },
+ "specialists/coldchain-v2": {
+ "sha256": "95ecd3302b85ada9304c0724c852f32c5173cc491cd0e1ad07bceff35df55701",
+ "files": 12
+ },
+ "specialists/cooking": {
+ "sha256": "b95cdb478d919ff54af26ec8275dce278e0365e5c576c79b2b4cdbc2fffe9a4f",
+ "files": 9
+ },
+ "specialists/cooking-v2": {
+ "sha256": "59d8a815a76d2240161f1c429bb185e1cf4ae89f7433c7207e156104189b9e12",
+ "files": 8
+ },
+ "specialists/cooking-v3": {
+ "sha256": "244f4d1225b884d24ed81138b885bbc4b60e48e91839aa3a55ce030980d46b3b",
+ "files": 9
+ },
+ "specialists/cooking-v3-r2": {
+ "sha256": "f9add0be09a177d4afb4485a4237474c0f6346baa3f9cdf5c98993c7c4a9b7d3",
+ "files": 15
+ },
+ "specialists/crafts": {
+ "sha256": "2d0867e2e54b1b6381f86f44999bb5fa9f84ae6a8f2cddc244bc67cba34557d7",
+ "files": 8
+ },
+ "specialists/cyber": {
+ "sha256": "948e2190f06c367417c08ec2e1dbf63eadd8826885743381805e61fa98d135a2",
+ "files": 8
+ },
+ "specialists/dataformats": {
+ "sha256": "2b97a2b0b9f9335f17541168e801ece056eef7b3ee7f42d6c5fb856dd968520c",
+ "files": 7
+ },
+ "specialists/decay": {
+ "sha256": "11ba66e2158cf17ea81814f8365350a73ee5fa531a2e61d094c93c1dc8e9b650",
+ "files": 8
+ },
+ "specialists/densities": {
+ "sha256": "d694151f731a175b784d756e84996ae32151c50314e49e46e30a72a5c1a211dc",
+ "files": 8
+ },
+ "specialists/densities-r2": {
+ "sha256": "afdd31a9495e7a54695bf3bb8a0d51eeb545ef82b7b9fcaa71367066c385498d",
+ "files": 9
+ },
+ "specialists/ecology": {
+ "sha256": "010360a1fdeb98a4f2fc21be6b1eebd96695d5d80e953b55f765bfc0a2b36b85",
+ "files": 8
+ },
+ "specialists/economics": {
+ "sha256": "eebb751f4324f72375b60a3d70942c8ab5b132331cb148cb83b9523ecad23bf4",
+ "files": 8
+ },
+ "specialists/electrical": {
+ "sha256": "4c89aed30761c2ac9d724bbf296c29d0c22305f6457b95a728cfb529381df4d1",
+ "files": 8
+ },
+ "specialists/electronics": {
+ "sha256": "9b37becbb4fba1243237e17a3291ddb6564e49041219519bd0d945890acd66dc",
+ "files": 7
+ },
+ "specialists/electronics-r2": {
+ "sha256": "1a67a09f49f11ed7b7bde0f73d4f98e1ed6e5e9ba5ca0eadf045f2fd14a90d68",
+ "files": 7
+ },
+ "specialists/embedded": {
+ "sha256": "d2acd2482059890e52b3685b527bac3d4d50622a524492c7487a51e2ce9aea5d",
+ "files": 8
+ },
+ "specialists/energy-v2": {
+ "sha256": "cccd99903b4f425a4c2506321e563ad3f382119ede0c3594304de7b1805dac52",
+ "files": 7
+ },
+ "specialists/energy-v3": {
+ "sha256": "9384e2a81a7042d11f4b7a0d3a2ff0a3b9b5b5f3edde8c1b7a35b1217094508b",
+ "files": 8
+ },
+ "specialists/entomology": {
+ "sha256": "ee081a71c75d7643f1b50729acca7fb0f002e30da7cdcfc8e9364da5209ee0fe",
+ "files": 10
+ },
+ "specialists/etiquette": {
+ "sha256": "ae8ee5a60711c2a49ec2f0ebf9baf119e0f1511e14cf530b2c8b6cd0d8ea7837",
+ "files": 10
+ },
+ "specialists/finance": {
+ "sha256": "ab5cb2ac863d2a679816e4c68b54dc55cce783bbc52b83069938397166c888c6",
+ "files": 7
+ },
+ "specialists/finance-v2": {
+ "sha256": "d3b5d3a6666a6f051cb7290f4967ebc6d2d16c68f8dd24235a2fac070ed96c00",
+ "files": 12
+ },
+ "specialists/finance-v2-r2": {
+ "sha256": "4b44d6264bc12075f7bf197726399559e6086a902a44de5d8c874cdae710bdf2",
+ "files": 11
+ },
+ "specialists/firesafety-v2": {
+ "sha256": "82dacf7eae4f061981dda17272e6572983e3972162224e9f24293fe387021b61",
+ "files": 10
+ },
+ "specialists/firstaid": {
+ "sha256": "ea314183893b08a18700995fb64ade8a45cc041f70ed30bdf93d094fe95f536b",
+ "files": 10
+ },
+ "specialists/fleet": {
+ "sha256": "304b6bb4a25fdff30ba018c17d527ad7d50405d20de20793af73d56985f4de2f",
+ "files": 7
+ },
+ "specialists/fluids": {
+ "sha256": "239fe31ee98b73988dd240b05e7f9d79e1f67d58c02a8a64d4faf8a0d14df5bb",
+ "files": 8
+ },
+ "specialists/fluids-r2": {
+ "sha256": "d93d4dc0f7777b2b6c73d44ac8d16e38cd08a69a92c8af2694da2ae63cee67db",
+ "files": 9
+ },
+ "specialists/forestry": {
+ "sha256": "b04fb4b96035ce10bb07c9f409dc309601f9a42cf8be8aed1d949090cec18bb7",
+ "files": 8
+ },
+ "specialists/fuels": {
+ "sha256": "03ce57c8ae111136fff26dffc6bff71a47bc6bb2df59d3a8c80767b5997d7d4b",
+ "files": 8
+ },
+ "specialists/games-v3": {
+ "sha256": "d0670dd8f9d94eb416999bd7048f4be96e91eff2702fad27636059275da47d58",
+ "files": 10
+ },
+ "specialists/gardening": {
+ "sha256": "8a64598bcf8beb10b3214dcbb36c2b29e588eabb00c41f3eb985f3e279c27f7d",
+ "files": 8
+ },
+ "specialists/genetics-v2": {
+ "sha256": "49ac519b2cc0eadeafb79b29c15c9a99e5d868b067fc6777ed065a9bc3a971d1",
+ "files": 11
+ },
+ "specialists/geography": {
+ "sha256": "8f107fcf17a048377317a4343411c8a6a8f75958282a266cc8fd15f8042a4197",
+ "files": 8
+ },
+ "specialists/geology": {
+ "sha256": "ad27f3d878b6de6f3b5b277b1a852625ebf12e0189d496d0acf90131f5461934",
+ "files": 8
+ },
+ "specialists/geometry": {
+ "sha256": "46fc4fafd8d3633ba3f3e15704733b51935a7b559995bd3cb06d5f7de3533abe",
+ "files": 6
+ },
+ "specialists/geometry-v3": {
+ "sha256": "b8e9842ccc063bebc2fd705039fcf485abb008f58d3f453d8d790f38d268ea93",
+ "files": 9
+ },
+ "specialists/geometry-v4": {
+ "sha256": "296a480f352d91f13e7903a3b9baf1a97d77dd55d956bed247ddcaf04689e1be",
+ "files": 10
+ },
+ "specialists/git": {
+ "sha256": "827f404802ecd0ecfe65d089cd56e99ddfc57dbcb25b5e77dbe617fa2d531d24",
+ "files": 8
+ },
+ "specialists/government": {
+ "sha256": "9a0b18fb8d5a030bdf1b5ca601550d2235008a72df49d6c858e7893bb7c9fbfa",
+ "files": 8
+ },
+ "specialists/grammar": {
+ "sha256": "0909a6e4d8ba122ffafef397ea88dec36e245783e72822a0d4b510c95e40a973",
+ "files": 8
+ },
+ "specialists/greenhouse": {
+ "sha256": "87175794f5aa16de7195017f1df2db1a650f1465ca79fd6d43ea0300ac09cda0",
+ "files": 7
+ },
+ "specialists/greenhouse-r2": {
+ "sha256": "f49eeef1a0310637ebd9320e55897f2cbd987577a9c76d99c4b4112f62ca73bc",
+ "files": 7
+ },
+ "specialists/hvac": {
+ "sha256": "7b98d4af07c2ae6ac5f31db2b275c48c891db332150e323ea374bf668e16e464",
+ "files": 7
+ },
+ "specialists/hydrology": {
+ "sha256": "d51a7e6c7b581f92ac3bcef8289010f8388e05b471766830a91f19fe3926bcf3",
+ "files": 7
+ },
+ "specialists/iot": {
+ "sha256": "62ca53f84921ed51c1ee97bb3972520e19f385b6e644ea93ae77acf1fa9c1ccc",
+ "files": 8
+ },
+ "specialists/javascript": {
+ "sha256": "30b3d464c7008f6cbbe67dae3923a2b80058f8ae6d66fe95efb0f8fbfaa1efec",
+ "files": 11
+ },
+ "specialists/labsolutions": {
+ "sha256": "cb38e6bf06cee68d5bcdbe33499aa0fdd82ca9b48697f865064614a67b63b5fe",
+ "files": 10
+ },
+ "specialists/labsolutions-r2": {
+ "sha256": "eaea9b9f7122c65aa74cd56f6177fc661fc9e638a4f988d90899dd30b78f2031",
+ "files": 10
+ },
+ "specialists/law": {
+ "sha256": "d3bce2f7225ee7fe17e016c3ffe0954550058fab6d1609495a3bc329b7cdb1a1",
+ "files": 10
+ },
+ "specialists/library": {
+ "sha256": "eee1d02ca69f26536501d5ae7bf23bd52a0450602b30b8ad6d6eeadd38ea49e0",
+ "files": 8
+ },
+ "specialists/linguistics": {
+ "sha256": "84a7c56de847547353b88ead503c85f6400b3d4089acd514e639e898ab29da3b",
+ "files": 10
+ },
+ "specialists/linux-v2": {
+ "sha256": "b5444cafc7d204c02693c83631c56334ae1d788a1a2a0a9dc5d9c541d5d98914",
+ "files": 11
+ },
+ "specialists/literature-v2": {
+ "sha256": "5caa9188095408f3e34a30132d5db84417f462bd9d4abb69f5a89a9f69ef4461",
+ "files": 11
+ },
+ "specialists/logic-v3": {
+ "sha256": "ef4158bebab9248feb6559276e51932edd36157fb684ec7b9a6e5686aa0055ce",
+ "files": 11
+ },
+ "specialists/marinebio": {
+ "sha256": "00c693b22ac924df1f7c19973805cd98f47d23b8b81f506d960d826abd29b33b",
+ "files": 10
+ },
+ "specialists/materials-v2": {
+ "sha256": "9d1a16e250ffbde10dbe91676e76f5e0a572739d59a3b96f6bbee3ccb8b283e8",
+ "files": 9
+ },
+ "specialists/mechanisms": {
+ "sha256": "5cd545850c81615d97f4bba5cacc9a8c9d3cf485d4d99aa439d1a8a79412eb46",
+ "files": 6
+ },
+ "specialists/mechanisms-v2": {
+ "sha256": "f3ef1347aef9b07b687e1411e9137fb5e835641223b6bc693f41e96f3d700a84",
+ "files": 12
+ },
+ "specialists/medicine": {
+ "sha256": "b8bcdd6b3d47a130a3fa29c35124f0f10b63e236f8047101c4a0140732842f50",
+ "files": 10
+ },
+ "specialists/medicine-r2": {
+ "sha256": "8357c755727bdad3c118cbe7d5a8a32a51b97b03219e00ef0ecfffe0e719446e",
+ "files": 9
+ },
+ "specialists/meteorology": {
+ "sha256": "8f159bc6f48d5ea57291a942c9936eb443599bb3af6ffdce23940274a63bd0ba",
+ "files": 6
+ },
+ "specialists/microbiology-v2": {
+ "sha256": "6ec92110413981f55ec5a7fbb019635da501f67347d2dca8698b2dc5fe41ad8f",
+ "files": 9
+ },
+ "specialists/music-v2": {
+ "sha256": "20a7d9f146a0ec2f32c9f461f7b4561146e9e3f68e68a1abc4befc5e19371e27",
+ "files": 11
+ },
+ "specialists/music-v2-r2": {
+ "sha256": "08a82efa304ff2257d64257ef8d171c4169e02f27c0859a20781c91eed631056",
+ "files": 10
+ },
+ "specialists/mycelial": {
+ "sha256": "7cd80e37e6e8bb57ecffa9df2ffaa02215f6163df59f334504b518efc9bcff29",
+ "files": 10
+ },
+ "specialists/mycology": {
+ "sha256": "470a4dfc6b5fdb86d9f7ebe5d4481aac518562e1d4b5fee4e329b0d10e1fe629",
+ "files": 10
+ },
+ "specialists/mythology": {
+ "sha256": "925cb2901a02ecad02a1dbdeeca739082096c4aa6077cf79c6db8badc8556c05",
+ "files": 8
+ },
+ "specialists/networking": {
+ "sha256": "a0a90634582cd4a45980ab9853e9e6b6f61a8638a4d904d7c9e1c9b64ee2cfdf",
+ "files": 8
+ },
+ "specialists/nutrition-label-v2": {
+ "sha256": "26d25c937ad6c22ede0b836b5de61786792f67ab1a9236de374c4cbc3a5187d1",
+ "files": 9
+ },
+ "specialists/ocean": {
+ "sha256": "145006ee7a909d93e90b23b4294691d453bef1d87579c8fa5e187ac1c51ce9d6",
+ "files": 8
+ },
+ "specialists/optics": {
+ "sha256": "c58517aa4565c85b7ecbbc9b8982bc53a04f050219014de01fce9e9ce3cec5be",
+ "files": 6
+ },
+ "specialists/optics-v2": {
+ "sha256": "38c66d52bcb457a5582abc5018a58e4119c1575e06d9267e225fdc8dea2b65ae",
+ "files": 7
+ },
+ "specialists/optics-v3": {
+ "sha256": "430447014db000b43e1fc857d9d5e5d8d0762cbfaf04002ce4f7784967f952af",
+ "files": 8
+ },
+ "specialists/optics-v5": {
+ "sha256": "8b97d434f613450ac163ab951d0ce74216987f0b60923e548f1d301a743bbb11",
+ "files": 21
+ },
+ "specialists/optics-v5-r2": {
+ "sha256": "cb66eb46e38433c478966af95f638fb44be4ae215509d2672965c27e408e36b4",
+ "files": 20
+ },
+ "specialists/orbits": {
+ "sha256": "2461710b2bfbe5d629c1afc92b64deffdadb8398222cbbd356a8c14be472582f",
+ "files": 7
+ },
+ "specialists/ornithology": {
+ "sha256": "80d51be829aab8c05fcc9f3654b31b59a5c8e82b8859730db133263841e94cd1",
+ "files": 10
+ },
+ "specialists/pets": {
+ "sha256": "50c22fb1a909e9870c762f22e73ff57d68091c7c5f1a211e54a8186787c1f1ac",
+ "files": 10
+ },
+ "specialists/physics": {
+ "sha256": "83ac3e58e64678b00b106e08e2c897b5bfadbb7c807deeb04141f46d4f243cd6",
+ "files": 7
+ },
+ "specialists/physics-r2": {
+ "sha256": "69e7990f3684ee2a43f8ab29a6c45c4df66416842b89c0bdc09e859840bb5d04",
+ "files": 9
+ },
+ "specialists/physics-r3": {
+ "sha256": "9e0ede142f1cecc4d27628a4ad53ad5b528af31557dbbfb2d05d2ff71172ede5",
+ "files": 8
+ },
+ "specialists/planetary": {
+ "sha256": "4b62d2f5c18fe869ad2a570fc22d4a4d94a735d178372e09f01c6f310af02459",
+ "files": 8
+ },
+ "specialists/poetry": {
+ "sha256": "9361a2a7b18f5bd260f57a9d417e62ebb00d9721625b2f4f99bbfd296aad44f3",
+ "files": 10
+ },
+ "specialists/printing": {
+ "sha256": "8f3a93b080db2a923196d2e6138949518c39cded5b800a4fb38e8a254e095df2",
+ "files": 7
+ },
+ "specialists/printing-v2": {
+ "sha256": "24b2fc335c8fde6a749e0104d8dbbf20d1f535aad854e160305442ab0e1d1e32",
+ "files": 12
+ },
+ "specialists/psychology": {
+ "sha256": "18061dc814732e42e51bb2a12ebc323d7169dd21c4734bd2cc0d651fe5052c98",
+ "files": 10
+ },
+ "specialists/python": {
+ "sha256": "4bbb3917b28303f8c1448f403ba2c0426b1528c199921ab8f7110a99b95b3bda",
+ "files": 9
+ },
+ "specialists/python-r2": {
+ "sha256": "1301cb2d6653db2a3a8488e9c5585426f63a537f8e3a4fa5e03a22e59c340365",
+ "files": 9
+ },
+ "specialists/rel-actors": {
+ "sha256": "94fe86c201fb2a0dd0036c642ab2a58040ce675a8ef7314fcc33be7218e1cb91",
+ "files": 8
+ },
+ "specialists/rel-aircraft": {
+ "sha256": "e42af0d661a5573d11d6d56b2fa28fc834c55099e23f5c16a2c674071695afd6",
+ "files": 7
+ },
+ "specialists/rel-airlines": {
+ "sha256": "8e1b16921e092b8ae91aaac84c60d2bb4fdb32dc9ef6f15f55f1a7786830c066",
+ "files": 7
+ },
+ "specialists/rel-airports": {
+ "sha256": "ca6c24da1cf6998f80ee384be4dc12323cb897a0fdb22f16503f6e5f68dcf57a",
+ "files": 7
+ },
+ "specialists/rel-albums": {
+ "sha256": "c50c357436987aa8974701d664e3db518a6751ed74438df38cc60441ade9869d",
+ "files": 8
+ },
+ "specialists/rel-animators": {
+ "sha256": "d6e6271266b0954a61471a5033b4af17c6c8a2b85c80be20f9430436c254590c",
+ "files": 7
+ },
+ "specialists/rel-anime": {
+ "sha256": "4d346b7a3ea5d1f9e24388099c3d89a060f554e767cfb91061e089ba7ce70aa3",
+ "files": 7
+ },
+ "specialists/rel-architects": {
+ "sha256": "327e99d5643a7de485d870acddde728a7650fba1c12448d51efa74638a853c98",
+ "files": 7
+ },
+ "specialists/rel-asteroids": {
+ "sha256": "b1840aebe96ea41b14a4fbfd5097a8500057e1d61e631ae332186dd6665c2598",
+ "files": 7
+ },
+ "specialists/rel-astronauts": {
+ "sha256": "82906463e7dae7f3f3232f660a9ccbedfc16b00a089c97de5682488561285152",
+ "files": 7
+ },
+ "specialists/rel-astronomers": {
+ "sha256": "62c8618d70496ee821bf2a9a44874c7d23e858af47887553ce1acaf7d5c91c6e",
+ "files": 7
+ },
+ "specialists/rel-athletes": {
+ "sha256": "e6f08e57dcda4107517e0dbf0fa730ef19be1389f806fd581373a8d60efafddb",
+ "files": 7
+ },
+ "specialists/rel-awards": {
+ "sha256": "192510470bd6e5e3f5cd4334529d75e3da99e1d034159351e65410152c4d42db",
+ "files": 7
+ },
+ "specialists/rel-ballets": {
+ "sha256": "4594970aa6fe14079921de49133551361a5d3f12447a3e848122aed14dd0ca5f",
+ "files": 7
+ },
+ "specialists/rel-bands": {
+ "sha256": "8c08916c112cf7010df395203a6f5a7d7c8598fddb5acf3eb31bb814c8e23e9b",
+ "files": 7
+ },
+ "specialists/rel-baseball": {
+ "sha256": "3d02b894c816062ed0533f17ada843533c1f639d18a701ceb749f5307e3c6857",
+ "files": 7
+ },
+ "specialists/rel-basketball": {
+ "sha256": "fc7872886bcb867f0796a456440b0b19040eb919ba05e3bd44f1fb5b0af7d0ac",
+ "files": 7
+ },
+ "specialists/rel-battles": {
+ "sha256": "92df9a44e245e60fc1bbc89bffa8b03f0d1c488462792b6935281f029aa2ef49",
+ "files": 7
+ },
+ "specialists/rel-biathletes": {
+ "sha256": "a71c3137c5c346185718b2562f38f36b02e83c3dba0cee2a287496b31a11e980",
+ "files": 7
+ },
+ "specialists/rel-biologists": {
+ "sha256": "a0a2953c7bc2c03f3f81258971699d318ec668dced7bd38aea6302a972ec6ff3",
+ "files": 7
+ },
+ "specialists/rel-bloggers": {
+ "sha256": "0f00be50c3df97466dc4c5cdfd67182f0ef7ec15072c558fe88020992392710c",
+ "files": 7
+ },
+ "specialists/rel-boxers": {
+ "sha256": "d64565a0cefb73a2b469a719e1e3be0033ee1f22f03d2bd5513d4bb47f26a228",
+ "files": 7
+ },
+ "specialists/rel-brands": {
+ "sha256": "8b96fe11f49ab3873f7ea817b6bd5515f993478d0a58cc5b790026f7915610c5",
+ "files": 7
+ },
+ "specialists/rel-breeds": {
+ "sha256": "1790526f1ffe51704dd62f5aa2b3e3ce4d7d1c66cedb851e63574eba17cc1bc9",
+ "files": 7
+ },
+ "specialists/rel-bridges": {
+ "sha256": "2b3def52a1d68670ee02baf4e2d022d1d6a405bac2bbe128d23e38771c1f5fe3",
+ "files": 7
+ },
+ "specialists/rel-business": {
+ "sha256": "e21c10c79e71e9225bd1deda100094bfcf110e6d88aaf528839ff3f2a475e510",
+ "files": 7
+ },
+ "specialists/rel-carbrands": {
+ "sha256": "96e3ec5eb9be9c0f372cdf71945bd221fac92c6531e03252c9c29ff48723c987",
+ "files": 7
+ },
+ "specialists/rel-carmodels": {
+ "sha256": "a6973c624b643c5c35887c987cfec8aa192e7895759e92b2521ed7a9ce267f92",
+ "files": 7
+ },
+ "specialists/rel-castles": {
+ "sha256": "6a64c827dae572dc4751559f981844123be6c362788b930055fb26d7f1f7b8a8",
+ "files": 7
+ },
+ "specialists/rel-channels": {
+ "sha256": "f1d7f551e22aa47982d1d60cd81ce037849fefc2709505d7dd0496dda01abef3",
+ "files": 7
+ },
+ "specialists/rel-characters": {
+ "sha256": "b0832b9c28a54c0e67ff794155e9dff3a700c6722a71d5dcf545d17e855c25ee",
+ "files": 7
+ },
+ "specialists/rel-chefs": {
+ "sha256": "7bf2cc4f04d36d90749d835d63a9dc1192b7cecb441aadb7397c5d3ad58aed3a",
+ "files": 7
+ },
+ "specialists/rel-chemists": {
+ "sha256": "ee488eaa75e596a6da2352f053e7a68d4bc6babea88d5b28801292bfdc75cadd",
+ "files": 7
+ },
+ "specialists/rel-chess": {
+ "sha256": "9e4c09d7a15ed6091047313e8249c021ad84ef1fcfbcb7d4b5de1c1012371ce8",
+ "files": 7
+ },
+ "specialists/rel-churches": {
+ "sha256": "00a55b2ff9b6e6d04b8dd3d89d7cc0ee67479e182e1d0c7a8e1976b532aaeda1",
+ "files": 7
+ },
+ "specialists/rel-comedians": {
+ "sha256": "c53bf97b567c0eab0348ebe2382762649f2b19078d02bf7f155286efce161e91",
+ "files": 7
+ },
+ "specialists/rel-comics": {
+ "sha256": "163c63c2c01975950728a4c9542f17a060355047f60147af46e5ca7f6cc75784",
+ "files": 7
+ },
+ "specialists/rel-companies": {
+ "sha256": "cfd7c900d24ca91e3da57d83fff03b3cec75fc9039b81a396ac085c4ad1237e3",
+ "files": 8
+ },
+ "specialists/rel-composers": {
+ "sha256": "c79bf75790d2f191fde1c581653601422a095cd0f28eded9dc402d722ef402af",
+ "files": 8
+ },
+ "specialists/rel-compounds": {
+ "sha256": "37ab98152a2eddbd791a059d3770573d16739fb228944811998a21cf9479c5dd",
+ "files": 7
+ },
+ "specialists/rel-computerscientists": {
+ "sha256": "4a5b0bb86e620a1a7bcfcb379058f5de7f08696adc83a173f3b342184e1c4b91",
+ "files": 7
+ },
+ "specialists/rel-countries-v2": {
+ "sha256": "93ea831614d96da5b335cedc51d1c92353630f715e5249639a5dc4b8044dc44e",
+ "files": 7
+ },
+ "specialists/rel-cricketers": {
+ "sha256": "6de3e498b4f94f57e7e5264dd600e2105f4ea072512e5bc17274d8e4d419c56f",
+ "files": 7
+ },
+ "specialists/rel-criminals": {
+ "sha256": "933cf3cbb1f84245ae10b5c8426444a8217f46a0b592c5c9a64db312bee20a5b",
+ "files": 7
+ },
+ "specialists/rel-crosscountry": {
+ "sha256": "057b79e8df90549b7889cc6585ddda55d9849a40093d1468ac19f4e6ea799dff",
+ "files": 7
+ },
+ "specialists/rel-curlers": {
+ "sha256": "6ee3c62c9a64cd24778c7c380902cd3e3946b8f0e854542537e28fa624a7c574",
+ "files": 7
+ },
+ "specialists/rel-cyclists": {
+ "sha256": "1d6a7ca32f895b42d764c8cc4dfe17b2105e2554ea8072864f5616b8e481a989",
+ "files": 7
+ },
+ "specialists/rel-dancers": {
+ "sha256": "cb3e7fde26a065c85e2381527fa96c7d0e097ba4606b7f6897b57fbff2fed531",
+ "files": 7
+ },
+ "specialists/rel-diplomats": {
+ "sha256": "1839e4fcdbd3c71126dbf4d0c3c4cb2e18b03eba4e4bf0d0bddc62365c1030c0",
+ "files": 7
+ },
+ "specialists/rel-directors": {
+ "sha256": "84cf854af587ec6b6f9b39002dffefa359411eb29df6cb7ffecbf23e43e3ff45",
+ "files": 7
+ },
+ "specialists/rel-diseases": {
+ "sha256": "5a7aec6b1cc06ed54e7cafa20ee196980931b050439b840f31e243492cac40b5",
+ "files": 7
+ },
+ "specialists/rel-dishes": {
+ "sha256": "d848aeb7cdf48c83e609fa61b1005ce7405fad6b08ff263957b616ce7dd20490",
+ "files": 7
+ },
+ "specialists/rel-drivers": {
+ "sha256": "7b735c16b6d7c66b0107f920106af435a8e8f8dee37f1f4dbbbf93dd0f82b52d",
+ "files": 7
+ },
+ "specialists/rel-economists": {
+ "sha256": "b1c909dcfe33a4b49c9d5afd2318d42bdf8f8cc1ede449ecf80158431f1331a0",
+ "files": 8
+ },
+ "specialists/rel-editors": {
+ "sha256": "3733af233905958f1cb006d88a2bb4b5bfae6a825142a7780032505c2537fcfb",
+ "files": 7
+ },
+ "specialists/rel-engineers": {
+ "sha256": "e3b7b616f3cec5e8fd2b5141742518af5c2f541e99a34928b4a3f330ea974809",
+ "files": 7
+ },
+ "specialists/rel-equestrians": {
+ "sha256": "d3f39cb631ec5b809c1ca0bd1117c4fe8e87863d7ddbb3166e912f103496a572",
+ "files": 7
+ },
+ "specialists/rel-explorers": {
+ "sha256": "679fa18e526a5991e18d81c532ca483e43d83e3a367196265fbbe7c375f80113",
+ "files": 7
+ },
+ "specialists/rel-fencers": {
+ "sha256": "0d0f2f742c7f542b51e963256a50541297ff0431235230a84e0f701eb5234eb1",
+ "files": 7
+ },
+ "specialists/rel-fieldhockey": {
+ "sha256": "1d2bc0be1741158c4b603ed2412dc2a83672303177d8a8e638fb04a71ccaad97",
+ "files": 7
+ },
+ "specialists/rel-films-v2": {
+ "sha256": "8bf0f56ee0465e1337ee635872dd7ae2beb20eef207ae4f9f75ff9d2fa48aabf",
+ "files": 9
+ },
+ "specialists/rel-footballclubs": {
+ "sha256": "ea9ac92911c3e8f3452a717bad1ca1c94862a9fa3c1889c046b83de118298f56",
+ "files": 7
+ },
+ "specialists/rel-footballers": {
+ "sha256": "dc44ded22167daf8ae3dfbee49baa4145df9186bfc09cfb8ad896032c778f65e",
+ "files": 7
+ },
+ "specialists/rel-galaxies": {
+ "sha256": "67e0fe8d8f49ed80c8de68992bcacea37afc8491de97d25231e3cbf4bff6c6f6",
+ "files": 7
+ },
+ "specialists/rel-genres": {
+ "sha256": "74b36f67e6661c2670a937bd4e575a4182aa922d188058a43333e35bb5c28c32",
+ "files": 7
+ },
+ "specialists/rel-geologists": {
+ "sha256": "2c5c60e548a0e16a8a89f60770d684e573a9721dbac30b0db57b24536be44816",
+ "files": 7
+ },
+ "specialists/rel-golfers": {
+ "sha256": "9dc2751c6517141b4d3f765cf327028403f5f0b0b16d7c3a5809e1f49a78c729",
+ "files": 7
+ },
+ "specialists/rel-graphicartists": {
+ "sha256": "3ea1c2264c144ba6256ba13313ba355cd4cbbd694bbdc56c47f0de3d74079b0e",
+ "files": 7
+ },
+ "specialists/rel-gridiron": {
+ "sha256": "0557820262e39c91d450034c680a66d8888254689cb177d89e6632f3bff284f2",
+ "files": 7
+ },
+ "specialists/rel-gymnasts": {
+ "sha256": "c33611064eed069becddc32e5bcdde210226aa6f2b045abeb4984d31204e3eb3",
+ "files": 7
+ },
+ "specialists/rel-handball": {
+ "sha256": "243248170ce1f8995dba7ad368a169322d8c7f24423d2ebaaf8ec9bf5ee7a12e",
+ "files": 7
+ },
+ "specialists/rel-historians": {
+ "sha256": "16dacace7a5f3382edc0b4e9d4b67d142ea7b391daacdef5a8b71f525225fca7",
+ "files": 7
+ },
+ "specialists/rel-hotels": {
+ "sha256": "e7f1c98da12c0584e72059252f0b3be6064785c36e2a6fdf2f5a70e963d6076c",
+ "files": 7
+ },
+ "specialists/rel-icehockey": {
+ "sha256": "f7390cf5e6ca8b34fe403fa3ba5aed9207583905e0dff56873d37687374ece77",
+ "files": 7
+ },
+ "specialists/rel-illustrators": {
+ "sha256": "ef869bf48ae791c837978b4fea4af90f07a56581d6f58fe230a457eae230b2d2",
+ "files": 7
+ },
+ "specialists/rel-instruments": {
+ "sha256": "055b9ca6ace969878484b791861bbd3380dc820c6f7a98bdbe4395ba34c35dfd",
+ "files": 7
+ },
+ "specialists/rel-islands": {
+ "sha256": "1f192f4ff7a3d12953aaf63e591665373687b0e472142d54112931f4d02b3486",
+ "files": 8
+ },
+ "specialists/rel-journalists": {
+ "sha256": "1623832e744ec02f5655c1ec52e7f8103a58311707c224e63936ef814cda5c33",
+ "files": 7
+ },
+ "specialists/rel-lakes": {
+ "sha256": "3247338c76ebde1e5d16915cdfed93465ed67f8b1f35af7d514c6a9121990a69",
+ "files": 8
+ },
+ "specialists/rel-languages": {
+ "sha256": "e9811041f02547874df89d43c661b8a4396f8a10ce9295b7ef01d9a5510c7c9f",
+ "files": 7
+ },
+ "specialists/rel-lawyers": {
+ "sha256": "f96279a7838e5a5e77817a058dcac70425f9fc125cf2add388bce32c757b39a4",
+ "files": 8
+ },
+ "specialists/rel-leagues": {
+ "sha256": "a13e9da52427806228a2c99547f1897697441c544029a6d5f22524790e576a98",
+ "files": 7
+ },
+ "specialists/rel-libraries": {
+ "sha256": "5e27954b7de3513ff431b67f384a40883722da86efabace23c7ef6cd193f6182",
+ "files": 7
+ },
+ "specialists/rel-linguists": {
+ "sha256": "a29a547cab9b9f725f3d1b8a18e0b2d36ac274b7c7802b25558d839030bf5e11",
+ "files": 7
+ },
+ "specialists/rel-mathematicians": {
+ "sha256": "79bb9d371e32fc15e7624676a121bc2b8bd7057a10affad7fa9f77dcd5689b76",
+ "files": 7
+ },
+ "specialists/rel-military": {
+ "sha256": "8795c7ffa35d7f07fbe1ec1114b12fd797449e78fa1294e016d2c91ab1b968e4",
+ "files": 8
+ },
+ "specialists/rel-militaryunits": {
+ "sha256": "044e30790c3b0f21cf95aaea70077fb79d856b59ef595667db1c38d475c0ba0e",
+ "files": 7
+ },
+ "specialists/rel-missionaries": {
+ "sha256": "ca313fbba00e276e81d2f20d62d1f50e0d21c07ca7c3574e30aca3409442f46d",
+ "files": 7
+ },
+ "specialists/rel-models": {
+ "sha256": "b8288521332328bf6e183b9aaff07215fa1dc1540c67a7eeab52d7b7b1247cec",
+ "files": 7
+ },
+ "specialists/rel-monarchs": {
+ "sha256": "701574bc7ab74b21c56c38f91d97ceecb8f04fab30bab83a0c5cef7e8103c7d6",
+ "files": 7
+ },
+ "specialists/rel-monuments": {
+ "sha256": "f611643d6dafbd2bf1fb2a2a106d47f304e45cc2c56b8e45d03b0255d842d763",
+ "files": 7
+ },
+ "specialists/rel-moons": {
+ "sha256": "1a8d7684d39d14f24f991eb98c845297327897725ba2033b8beecabf518797ee",
+ "files": 7
+ },
+ "specialists/rel-mountains": {
+ "sha256": "d300ef909811e4ae2f50193cc48902dcfb1698dfa3b6146a0e00a3e6d1834764",
+ "files": 8
+ },
+ "specialists/rel-museums": {
+ "sha256": "4911f8d7c96a805d179b9df6135e12465505f8380ab6b005dbeaee70b5f0247f",
+ "files": 8
+ },
+ "specialists/rel-musicians": {
+ "sha256": "bc5f3a1ca2d92da54a653cfb9a3e6f3d1c2b9dfe7ece6346e30b4b0b9ee4f907",
+ "files": 7
+ },
+ "specialists/rel-mythology": {
+ "sha256": "5cdd880a0ea4b24df5ca559e589dca77e1e1b6964bd446b7a245cb302e5c365b",
+ "files": 7
+ },
+ "specialists/rel-newspapers": {
+ "sha256": "95aa11a1ac5ab8a417e88341e8a45d2b10ec1947b2beeb45efe7ca11df47bc07",
+ "files": 7
+ },
+ "specialists/rel-novelists": {
+ "sha256": "b39c6fe5e3ddff538106fc12b44bf952423ecc187f9a8cf9f12455c1b2fbbaea",
+ "files": 7
+ },
+ "specialists/rel-novels": {
+ "sha256": "a4b8f79229ff9aca030af4fc4adbb36e2c4f09a9d4bd5de24076391e7b338f9e",
+ "files": 8
+ },
+ "specialists/rel-nurses": {
+ "sha256": "4544d9f519a5bfdd1815a8e2a55f97f3c1e5d531e3bf274265ce428b73a6e7f0",
+ "files": 7
+ },
+ "specialists/rel-observatories": {
+ "sha256": "1071d6283696980ab1cf1c7946dda1dff4dc6b797bbdeb1546a26a6a14549869",
+ "files": 7
+ },
+ "specialists/rel-operas": {
+ "sha256": "29e188da5526917011bf8c5c098fe6b7d1a3e80de6390e8f3d7f208d5c312ca5",
+ "files": 8
+ },
+ "specialists/rel-organisations": {
+ "sha256": "d9089a0cf4f906878b29182d8e37eac4490fc09348eecf5f16c6e705a11ba33d",
+ "files": 8
+ },
+ "specialists/rel-painters": {
+ "sha256": "75cb8fc5278bec2b96de456d1194258fb1da833655eab77bf10a70fbf0427b75",
+ "files": 8
+ },
+ "specialists/rel-paintings": {
+ "sha256": "16c9088861b395a971ac920f5e77dfefc6a69bf99485721588f1530bb57fc628",
+ "files": 8
+ },
+ "specialists/rel-parks": {
+ "sha256": "ca33388f09a2b38097dfdbef5dd899bbf36721566545838279ccf8bb1a8074f8",
+ "files": 7
+ },
+ "specialists/rel-parties": {
+ "sha256": "9f0a12993d4601bb47ec06d8ec97f04c68d0b96f4f507ae356fb58f2dbea465e",
+ "files": 7
+ },
+ "specialists/rel-pharmacists": {
+ "sha256": "340f31fb64fed582db61ab873eeeffe4a06c9990011be2a021062fc69d20df67",
+ "files": 7
+ },
+ "specialists/rel-philosophers": {
+ "sha256": "f1603c6cd2433c36be0d43b066af9712e642de1224e0fb653c84061d3c6c4920",
+ "files": 8
+ },
+ "specialists/rel-photographers": {
+ "sha256": "bb78a17a208eb7f320ed3cf6e7ce288fbd504f14a6fddcf9b765ad723e59ca9d",
+ "files": 7
+ },
+ "specialists/rel-physicians": {
+ "sha256": "7c59128f951efc1ff71e5f48cd40ba04e9a82b6c07c405b18198b59b5af26bfe",
+ "files": 7
+ },
+ "specialists/rel-physicists": {
+ "sha256": "c7968132233d2b2f9b6562d92f7ddec84514fe7df861b5c3e99ed07155796526",
+ "files": 7
+ },
+ "specialists/rel-planets": {
+ "sha256": "e376ea62c8494cfa9f1d72c8ccbc3d3140265c9c7cf0c5edb1d5f044777ecc3e",
+ "files": 7
+ },
+ "specialists/rel-playwrights": {
+ "sha256": "66d8af86ec6e9afa791fa58f961f8e128854ba69de5fdbb66ebeea8fe36a303d",
+ "files": 7
+ },
+ "specialists/rel-poems": {
+ "sha256": "f1c3faeb97874b50356216ce700524b04d951f3bf0199773d303020c9cab5541",
+ "files": 7
+ },
+ "specialists/rel-poets": {
+ "sha256": "7b1da9e53e2777d33017333fd41a9f2d7746e42a6c5b3025c20a12243cfe7005",
+ "files": 7
+ },
+ "specialists/rel-police": {
+ "sha256": "48e51ce84a4bbcdf691deb4957723a6e0f820fc38ff0cc02675471be406b0537",
+ "files": 7
+ },
+ "specialists/rel-politicians": {
+ "sha256": "cd47b4099994c03f4c9c94dcb9bebecd8bc7e4a029643798b0d275df2fa2948e",
+ "files": 8
+ },
+ "specialists/rel-printmakers": {
+ "sha256": "7f06a971bf5082030c892b31443414f496995d5ccb15a2577dcdd3acdbfc3139",
+ "files": 7
+ },
+ "specialists/rel-psychologists": {
+ "sha256": "ea8eae6c8c0764b5122aa4fc818e4768478592f6b02e2f4cc5fcceeba4de254b",
+ "files": 7
+ },
+ "specialists/rel-racketsports": {
+ "sha256": "838c495f22fc6af5616c01587cceba530aaaa4357c77ebdb1feb23c33e74ad97",
+ "files": 7
+ },
+ "specialists/rel-religions": {
+ "sha256": "2a9010b3010abe5f1f2b4fa09edec6ff9fd6aece86fb069b9dc70fd9d2eae22b",
+ "files": 7
+ },
+ "specialists/rel-rivers": {
+ "sha256": "6b3aecaeacc410f023c9e6624cecd8d75c6c594fec768b64ee2791c22e59195d",
+ "files": 7
+ },
+ "specialists/rel-rowers": {
+ "sha256": "c510f97c0fe835dbf17e0bbf4b23d00e1c8e2cb098a766f3f72d16df3f35ba24",
+ "files": 7
+ },
+ "specialists/rel-rugby": {
+ "sha256": "6c5917549941cce449ceebae9aa7fa577d630aa76a236f9a5cae27943c31eefa",
+ "files": 7
+ },
+ "specialists/rel-sculptors": {
+ "sha256": "a8e4e840ef6bdf6b29a0531551ef70e3fd2f46f4fcd6ee93f67ee90d29a1c6e4",
+ "files": 7
+ },
+ "specialists/rel-sculptures": {
+ "sha256": "dbc7669363f77b507e1a8cdd96d5839a448be42adb1130975e7e29a2448e5dd8",
+ "files": 7
+ },
+ "specialists/rel-ships": {
+ "sha256": "511520e5bed4f002e4aaa537de028d2f28a3eb4f78ef4480e7e307a5ca4fc3be",
+ "files": 8
+ },
+ "specialists/rel-shooters": {
+ "sha256": "29f6d3fc50bd60d80bc2f14100e49dd42db7c4e081fc01632bb73bc556991afe",
+ "files": 7
+ },
+ "specialists/rel-singers": {
+ "sha256": "f6441ad65e08bfbca93614fcf88cc4e2293fc2578c5b9f63aeb3a80458c0ae4f",
+ "files": 8
+ },
+ "specialists/rel-skaters": {
+ "sha256": "01b47325741aaffc4f290b8ef480be3314ba4aa4d2593f01ff79d54eef6fb687",
+ "files": 7
+ },
+ "specialists/rel-skiers": {
+ "sha256": "153c984b861bf22c4defb1036461cdda906b1c37a595c59a38353eb3eb495480",
+ "files": 7
+ },
+ "specialists/rel-skyscrapers": {
+ "sha256": "7fdbc10b6514536caaf03d3d9aef367aebb75f63dd3899be773481f1a1ce7900",
+ "files": 7
+ },
+ "specialists/rel-snooker": {
+ "sha256": "c6f96d9e9608f01f58048033179210e6f18f8db650ca3729fe8fa078c9de6c4a",
+ "files": 7
+ },
+ "specialists/rel-software": {
+ "sha256": "ce2d3cf68062b42a5ba8b0802c6646058df162306b0955e72a5ba29194bb5dd9",
+ "files": 7
+ },
+ "specialists/rel-songs": {
+ "sha256": "cc5417c3dc1cc3d3a7bc6dffceb9f6d5b2b3fac466c4ddded9634f8ccf36cfa3",
+ "files": 8
+ },
+ "specialists/rel-spacecraft": {
+ "sha256": "e60b7516ac6ca848c688c1f68ca2081def7371b231c8317276884fee4c9a8820",
+ "files": 7
+ },
+ "specialists/rel-speedskaters": {
+ "sha256": "23d62e9f14f51e5666f530c341eacca5db7cf217149f874a88a2b143a2d241f0",
+ "files": 7
+ },
+ "specialists/rel-spies": {
+ "sha256": "d20f6ef2f652cb4509f449deef088f93b9c40c72bb5db14d60e33d553587a2cd",
+ "files": 7
+ },
+ "specialists/rel-sportsevents": {
+ "sha256": "dc0ff64f3a8198b528f97e4f0904bc32d57267892855d4eec3090a3b44dc5d5f",
+ "files": 7
+ },
+ "specialists/rel-sportsteams": {
+ "sha256": "8e5ec70c11be3aed82035d9e795e36ece07146f1315751ea9fcec579cbbfc686",
+ "files": 7
+ },
+ "specialists/rel-stadiums": {
+ "sha256": "a6921371c0c7bd6fb36dbb260b0df590138ad46e573394f391f9bd9ad35abf0f",
+ "files": 7
+ },
+ "specialists/rel-stations": {
+ "sha256": "7866fd03c4f16a321bc1f0f8c3bd4ae48238a7e5f21c7d04941272f7d4c440d3",
+ "files": 7
+ },
+ "specialists/rel-statisticians": {
+ "sha256": "76ca6babfb634212ae059a74f154c3fd6daf1d1aac1c98789fa64cf730d9993d",
+ "files": 7
+ },
+ "specialists/rel-swimmers": {
+ "sha256": "c22117266691d17b14b5d870762f763e1fe6ad4633d08fd8f6b4d573bac215ca",
+ "files": 7
+ },
+ "specialists/rel-temples": {
+ "sha256": "b541f2792e9e01fb0e489cecc75d9b5367c7c0d0f46ce03cd03d81244745db8c",
+ "files": 7
+ },
+ "specialists/rel-tennis": {
+ "sha256": "521d487542938f99097b9bbeea818e657c8aebd119c4e0b9aca6dbaac0d36674",
+ "files": 7
+ },
+ "specialists/rel-theatres": {
+ "sha256": "1d1a190b50f411513f02cb722f11b6f4af313180965d7578cdd4f8a7d10ed981",
+ "files": 7
+ },
+ "specialists/rel-themeparks": {
+ "sha256": "05504903e2f7d08ffde81e266e23df3ccb9dd49b2b62778c1b13a6faa022d550",
+ "files": 7
+ },
+ "specialists/rel-theologians": {
+ "sha256": "20ac3972bbf86bda3a30eebb583f3fb9167b5867d2ce4d3bb9caae1352d0dff0",
+ "files": 7
+ },
+ "specialists/rel-tradeunionists": {
+ "sha256": "45362aba3686c554fabe6ffc64c049dda9ab67e4f09ad3091215e58e40036cf5",
+ "files": 7
+ },
+ "specialists/rel-translators": {
+ "sha256": "35e5399bcacfa55d934779cf7ac302d76fb936b689ec06ae6c7afcc0f9172f1e",
+ "files": 7
+ },
+ "specialists/rel-triathletes": {
+ "sha256": "49096909d2b0d008e49a197525e55ec0ce11d75cf71d3bc05d0f37b9783ec5ae",
+ "files": 7
+ },
+ "specialists/rel-tvproducers": {
+ "sha256": "2abcbc61f8bed6170712577303cf46e62afb95d8e03e85a6882c3c3a8ba0edc9",
+ "files": 7
+ },
+ "specialists/rel-tvseries-v2": {
+ "sha256": "77cf98ad66bd4ca48ca85943f5c882a4542203f16bac999d55151c63846d7025",
+ "files": 9
+ },
+ "specialists/rel-universities": {
+ "sha256": "2510d9d003c1e7d603b065d746e341e604705b4e90d26734bb1734db5316f9af",
+ "files": 7
+ },
+ "specialists/rel-videogames": {
+ "sha256": "82619d9498cbeb235331a73f02c4e06bff2e7fa342881897572ab60c6a94688a",
+ "files": 7
+ },
+ "specialists/rel-volleyball": {
+ "sha256": "6e6fb9b90e50721338be6377df19fce8bd8d3dbdca1fdce6fe679a8893df5ed6",
+ "files": 7
+ },
+ "specialists/rel-wars": {
+ "sha256": "3ed991f4c8b9f23334647e13f4132e5eec541d54d1beced35834e955f241ae9f",
+ "files": 7
+ },
+ "specialists/rel-waterpolo": {
+ "sha256": "316111a529a8265b3047506eea793676084c3cb1a1c6c7655c88cf430b5ee64d",
+ "files": 7
+ },
+ "specialists/rel-youtubers": {
+ "sha256": "efba4c3d1fb5e7f8f9869fa0919a83eac5ee8f7a3b262d74db71b965e8055ae9",
+ "files": 7
+ },
+ "specialists/renewables": {
+ "sha256": "3e0b86a27cbfad6da5b373bd1198fbb757c2134221cd245006d737bcfbf0355d",
+ "files": 8
+ },
+ "specialists/retail": {
+ "sha256": "c94b6e73f60ad616d19187fe50f3ea81189b49bbaf5d8d6a68e620155fd81e17",
+ "files": 7
+ },
+ "specialists/robotics-v2": {
+ "sha256": "59ce39c28c72c2eb5b5afada758b45752a8721f2b997f81f9ea012bb3e14fe1e",
+ "files": 9
+ },
+ "specialists/safety": {
+ "sha256": "387631c63c91f42d05356f3ff4721a137981b77212c72e1eb5c3de64c1271c3b",
+ "files": 6
+ },
+ "specialists/seismology": {
+ "sha256": "6af36c14e4f9a6aee48bec7485f3dbe5f99909a3027be2622e9b4d3fa8f5c21b",
+ "files": 8
+ },
+ "specialists/sociology": {
+ "sha256": "b072aa03f2975aadad28fd7b0922ea4477c5506a7547abefe88c504ea07e7a47",
+ "files": 10
+ },
+ "specialists/soil": {
+ "sha256": "15e98ce12978b06259ca0b201755c00011a5f63d65a8c6ab33d10ff576ee5a95",
+ "files": 8
+ },
+ "specialists/soil-r2": {
+ "sha256": "640c83136e49d022eb15185667ab85aee534b02fc284a0650c64aea2bd24817e",
+ "files": 7
+ },
+ "specialists/spacecomms": {
+ "sha256": "dea586cd9ba7b0838c655a6abfd6ca1abd9d7eddb942a3daf7bb4cabc9f8a3ae",
+ "files": 7
+ },
+ "specialists/spacecraft": {
+ "sha256": "01b8b0ef55faa9ec6588a0ae206d9c775c4849da96d9186c913c8837cfb6f043",
+ "files": 8
+ },
+ "specialists/sports": {
+ "sha256": "7e24a125743bfab2b7959561564bcc13d3aed4ad19d325bfcfc3fd2ea19230cf",
+ "files": 8
+ },
+ "specialists/springs": {
+ "sha256": "f12cf342d028806ff93fe9f146cd2a1f8d383283695b38549b3c33fe5cb5247f",
+ "files": 8
+ },
+ "specialists/sql": {
+ "sha256": "ff5aa630476ab4c3523737c54952b94b2007c943fe7b90648107a8e162aee1bd",
+ "files": 6
+ },
+ "specialists/statistics": {
+ "sha256": "5448df943eaa687ec381deca697e3907d1910ceaa770cc30a8c02fb72d8e0913",
+ "files": 7
+ },
+ "specialists/statistics-v2": {
+ "sha256": "96a20825e3c70bc795945d5996dac2f21c3a181fb368821e83daabdd3e4b98c4",
+ "files": 7
+ },
+ "specialists/statistics-v3": {
+ "sha256": "88be2300583637eefdd14815a85fda4e0ac72d836b7cb48eb9d342cd84dbccb8",
+ "files": 8
+ },
+ "specialists/thermo": {
+ "sha256": "b92ad90fb4bca6d3e8eee4a36c3bb3d3499ab1c2ef84061f4783029f966a6602",
+ "files": 6
+ },
+ "specialists/units": {
+ "sha256": "20140ff7585313840468cdf3eca54355fab59d5c1313ae14ec37b21f8ae9fad4",
+ "files": 7
+ },
+ "specialists/units-v2": {
+ "sha256": "fdf2c8fd200c97508cc130b209a3f065fd3fe1f65c4254a3c48fc7891dce0c10",
+ "files": 8
+ },
+ "specialists/units-v3": {
+ "sha256": "f70b4d8d0c8c44295f373b693364194892084991c104465a0559a19a700cebad",
+ "files": 9
+ },
+ "specialists/units-v3-r2": {
+ "sha256": "86420f3c59b4e967b58c2f91dc8fdc2709ee3105947af839ae75553f66f885e7",
+ "files": 13
+ },
+ "specialists/ushistory": {
+ "sha256": "490526165f1b73b83ca15639af33b24e9601ec13d42c8652012efdc78e10e383",
+ "files": 8
+ },
+ "specialists/warehouse": {
+ "sha256": "8190fff2c6be02b5fb955df4f3bbaf8de35c75da260f83e041a3e6cb17a2ce17",
+ "files": 8
+ },
+ "specialists/warehouse-r2": {
+ "sha256": "8c1573c565ea4ba16970c319182bed17cc743baaa78a13e8c14c5cebd75d59e6",
+ "files": 10
+ },
+ "specialists/water": {
+ "sha256": "2361c83b7727d6b3278de13de173156f879db5592583be404a31b1723acc8998",
+ "files": 7
+ },
+ "specialists/water-r3": {
+ "sha256": "c9ec2b8be5326cc0bdd730ac382a23849ad4d976e44529c1ec8add1767971ce4",
+ "files": 8
+ },
+ "specialists/wildlife": {
+ "sha256": "643d940428a61215b4e1c20a90ee637681d318eb983ac2d873fa76cf0f8e57bf",
+ "files": 8
+ },
+ "specialists/wiring": {
+ "sha256": "2b04aeedfd914dddfa83139c32b59a3a43353f8f1b4eec90b0a9b3d958480c3d",
+ "files": 7
+ },
+ "specialists/woodworking": {
+ "sha256": "56ecadf3bb7c0116ccfd82bbaf60a0bbeececcd66e7db327d2516197243a0d5b",
+ "files": 8
+ },
+ "specialists/worldhistory": {
+ "sha256": "5b977e269d66669c9cb2aafad2e4782f709863553d9670e3113dd8fb53662c02",
+ "files": 9
+ },
+ "core/admitted": {
+ "sha256": "7771dbeabd010f8bc664ff367dc6b51015a8e3b20870fd1790f04f3a46098585",
+ "files": 341
+ },
+ "workbench": {
+ "sha256": "c9c21bebfb4ecf34fa0b598eb411b2a20d735f1b0e6277381fbf018f4b38c75f",
+ "files": 150
+ },
+ "oracle7.py": {
+ "sha256": "9d68865540b23122e0020c0b8ee52471a71a3dfa8e3572b23b961ea4b3bf920e",
+ "files": 1
+ },
+ "PACK_MANIFEST.json": {
+ "sha256": "1890ae3b3b754d901bf1cf552688eee5bc8acad8a148f08eadfb9fd6a614b132",
+ "files": 1
+ }
+ },
+ "recipe": "(the same code ships in workbench/verify.py, so users recompute it on their own install):\n part hash = SHA-256 over, for every file under the part's directory in byte-sorted relative POSIX path order (skipping\n __pycache__, *.pyc, verdict-free runtime state), the UTF-8 path, a NUL, the file's own SHA-256 (32 bytes)\n - files the download ships zstd-packed are hashed as their unpacked bytes (the PACK_MANIFEST sha256)\n leaf = SHA-256(UTF-8 \"oracle7-part:\" + name + \":\" + part hash hex)\n root = binary Merkle tree over the leaves in name order; a level with an odd count carries its last node up\n unchanged; a parent is SHA-256(left || right) of the raw 32-byte digests\nParts: every specialist directory (specialists/
), the specialists' gate tools, the admitted fold chain, the workbench,\nthe launcher. Stores are covered by PACK_MANIFEST.json, whose own hash is a part.",
+ "anchor": {
+ "chain": "solana-mainnet",
+ "program": "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr",
+ "memo": "Oracle7 release w548 3.1, 2026-10-17. oracle7 merkle c4c1edb342b54fe5faf7fdf775d27bf72d351ea0481d246c648a2e92b5269050. oracle7-slim merkle 732358ed179adc00f0404dc7ecb1fe7e11484699559f6c772a1c578b002288da. Recipe: RELEASE_HASHES.json in each repository.",
+ "signature": "4kW4S5V4kHsFKGcxWBn37jPD73nGP17aF8eF9yPv8dEkwzTbWksvYaej2VkiUPMQ9PVdN92mmve4tAqgsr6RZ7s3",
+ "block_time_utc": "2026-10-18T02:51:28Z",
+ "slot": 458275075,
+ "explorer": "https://explorer.solana.com/tx/4kW4S5V4kHsFKGcxWBn37jPD73nGP17aF8eF9yPv8dEkwzTbWksvYaej2VkiUPMQ9PVdN92mmve4tAqgsr6RZ7s3"
+ },
+ "supersedes": {
+ "merkle_root": "0575c9b78f6dcc0a48ee57e6dc63d3e1a46afc13d2c9d9d4503db6c784564ed2",
+ "anchor_signature": "WNEdPhKJZYCh3ZKe7X8zhVehuyskEnKuDbde86W1XdKdBC9izVAJTzbrewtSUQ8keQvT1rZUBumCGnGxjDXyVgU",
+ "reason": "release 3.1 (17 October 2026): fold composition - specialist network v6 composes operations learned from the specialists' own exams (w549); the specialist answers after a check stops a route that could not know (w548); 3.1 correctness rules from the research record's re-examination and composition rounds (w547); the Workbench's worked example is the habitable-zone specialist; release gate: sealed test 22."
+ }
+}
\ No newline at end of file
diff --git a/root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/HEAD_MANIFEST.json b/root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/HEAD_MANIFEST.json
index 26310ae96ecab9786c310f10895d7e26698f15c0..2cc8ae88cf3f69fb4f7913de678ebf7f6e61b063 100644
--- a/root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/HEAD_MANIFEST.json
+++ b/root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/HEAD_MANIFEST.json
@@ -1,6 +1,13 @@
{
- "built_ts": "2026-10-18T01:41:40Z",
+ "built_ts": "2026-10-19T15:34:05Z",
"chain_tip_to_base": [
+ "oracle_release_runtime_w558",
+ "oracle_release_runtime_w557",
+ "oracle_release_runtime_w556",
+ "oracle_release_runtime_w423",
+ "oracle_release_runtime_w555",
+ "oracle_release_runtime_w554",
+ "oracle_release_runtime_w551",
"oracle_release_runtime_w548",
"oracle_release_runtime_w547",
"oracle_release_runtime_w550",
@@ -351,5 +358,5 @@
"oracle_release_runtime_w3"
],
"schema_version": "oracle-head-manifest-v1",
- "tip": "oracle_release_runtime_w548"
+ "tip": "oracle_release_runtime_w558"
}
diff --git a/root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w423.py b/root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w423.py
new file mode 100644
index 0000000000000000000000000000000000000000..190b3288343c8fa515e8e2f179f374d7b9decb4f
--- /dev/null
+++ b/root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w423.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+"""oracle_release_runtime_w423 - POTENTIALS fold (epoch 98): the typed space of the possible.
+
+30 potentials bound from live sources: the self-model's declared gaps, the
+human-signed target-shape distances, pending fold families, unmapped induced templates
+(residuals naming missing capabilities), and refused candidates whose lessons are
+encoded. A potential CONVERTS to an actual when a fold family admits - the conversion
+rate across epochs is the measurable throughput of self-improvement.
+Authored 2026-10-18 16:44 UTC by oracle_fold_worker_v6, chained on oracle_release_runtime_w555."""
+
+from __future__ import annotations
+
+import re
+
+from oracle_release_runtime_w555 import OracleReleaseRuntimeW555
+
+POTENTIALS = [{'kind': 'instrument_missing', 'name': 'declared_implications_closed: build the measure (a route that declares an implication supplies it; )'}, {'kind': 'instrument_missing', 'name': 'panel_guard_rate: build the measure (an instrument a mutant can pass is not measuring w)'}, {'kind': 'instrument_missing', 'name': 'routes_graded: build the measure (ten of twenty eight routes are wholly ungraded, an)'}, {'kind': 'shape_distance', 'name': 'continuity 230 -> 260'}, {'kind': 'shape_distance', 'name': 'grounding_cited 0.4905 -> 0.75'}, {'kind': 'shape_distance', 'name': 'thread_depth 1.89 -> 4.0'}, {'kind': 'declared_gap', 'name': 'route:conceptnet_isa_derived missing complements'}, {'kind': 'declared_gap', 'name': 'route:conceptnet_isa_direct missing complements'}, {'kind': 'declared_gap', 'name': 'route:conceptnet_partof_derived missing complements'}, {'kind': 'declared_gap', 'name': 'route:conceptnet_partof_direct missing complements'}, {'kind': 'declared_gap', 'name': 'route:conceptnet_isa_derived missing mediated_by'}, {'kind': 'declared_gap', 'name': 'route:conceptnet_isa_direct missing mediated_by'}, {'kind': 'declared_gap', 'name': 'panel:stage5m30_oneshot_report_v1 unguarded vs STRIP_CITATIONS, DENY_P'}, {'kind': 'declared_gap', 'name': 'panel:stage5m31_oneshot_report_v1 unguarded vs STRIP_CITATIONS, DENY_P'}, {'kind': 'declared_gap', 'name': 'panel:stage5m32_oneshot_report_v1 unguarded vs STRIP_CITATIONS, DENY_P'}, {'kind': 'declared_gap', 'name': 'panel:stage5m33_oneshot_report_v1 unguarded vs STRIP_CITATIONS, DENY_P'}, {'kind': 'declared_gap', 'name': 'panel:stage5m34_oneshot_report_v1 unguarded vs STRIP_CITATIONS, DENY_P'}, {'kind': 'declared_gap', 'name': 'panel:stage5m35_oneshot_report_v1 unguarded vs STRIP_CITATIONS, DENY_P'}, {'kind': 'declared_gap', 'name': 'act:continue_topic'}, {'kind': 'declared_gap', 'name': 'act:refine'}, {'kind': 'declared_gap', 'name': 'capability:list_n'}, {'kind': 'declared_gap', 'name': 'capability:multi_step'}, {'kind': 'declared_gap', 'name': 'capability:timeline'}, {'kind': 'declared_gap', 'name': 'capability:what_if'}, {'kind': 'vessel', 'name': "vessel v259928 w=32042074008.0: You've reached Oracle7, an AI assistant | lacks bounds"}, {'kind': 'vessel', 'name': 'vessel v0163 w=3704609090.0: where are you going ? | lacks bounds'}, {'kind': 'vessel', 'name': 'vessel v0145 w=3645804780.0: what do you believe ? | lacks bounds'}, {'kind': 'vessel', 'name': 'vessel v0096 w=2147441940.0: what have you seen out there ? | lacks bounds'}, {'kind': 'vessel', 'name': 'vessel v0099 w=2129776350.0: what do you make of this place ? | lacks bounds'}, {'kind': 'vessel', 'name': 'vessel v0098 w=5506062.0: what will you make next ? | lacks bounds'}]
+
+POT_RX = re.compile(
+ r"(what (?:could|can) you (?:learn|become)|what are your potentials"
+ r"|what is potentially possible|what might you become)", re.I)
+
+
+class OracleReleaseRuntimeW423(OracleReleaseRuntimeW555):
+ """Base + the potentials route."""
+
+ def chat(self, text: str, session_id: str = "default") -> dict:
+ s = " ".join(str(text).strip().split())
+ if POT_RX.search(s):
+ bits = "; ".join(f"{p['kind']}: {p['name']}" for p in POTENTIALS[:6])
+ return {"status": "answered", "path": "worker:potentials",
+ "response": f"By my own ledgers, {len(POTENTIALS)} typed "
+ f"potentials stand open, among them {bits}. Each "
+ f"converts to an actual only through the gate. "
+ f"[potentials_epoch_98]",
+ "provenance": {"kind": "potentials_ledger", "epoch": 98,
+ "count": len(POTENTIALS)},
+ "latency_ms": 1}
+ return super().chat(s, session_id)
diff --git a/root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w551.py b/root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w551.py
new file mode 100644
index 0000000000000000000000000000000000000000..4f637ababc8c9a55b4f5f1290d653a4ed8590e06
--- /dev/null
+++ b/root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w551.py
@@ -0,0 +1,152 @@
+#!/usr/bin/env python3
+"""oracle_release_runtime_w551 - CORRECTNESS RELEASE (3.2): THE PROVED CROSS-CHECK.
+
+veri7vn (github.com/emberian/veri7vn, CC0, by ember arlynx; our fork github.com/psiloceyeben/veri7vn) re-implements 117 of
+Oracle7's released specialists as kernels written and proved in Isabelle/HOL, exported to Standard ML and compiled with
+MLton. Each kernel answers by the answerer contract (one JSON line in, one out) and names the theorems its answer rests on.
+On the research box, over the released specialists' own held-out exams (6,444 questions), the kernels and the Python
+specialists gave the same answer value on every question both answered, apart from display rounding (the kernels round
+exact rationals; a Python float can store 0.675 as 0.67499...).
+
+After the parent turn, when the reply came from ONE named specialist (path "specialist:") that has a kernel:
+ - the kernel is asked the same question;
+ - it agrees (same Yes/No opening, and the first stand-alone number equal within one unit of the reply's last shown
+ decimal or 0.5%) -> the reply is UNCHANGED, byte for byte; its path gains ">proved:" and the reply carries
+ "proved": {kernel, session, theorems};
+ - it gives a different answer -> the reply is withheld, and says a proved check disagreed;
+ - it declines, errors, times out, or there is no kernel or no Linux x86-64 to run it on -> the reply is unchanged.
+So the fold can only add a mark of proof, or withhold an answer a proved kernel contradicts. Kernels are static Linux
+x86-64 binaries under specialists/_proved/bin (SHA256SUMS, PROVED.json: source commit, licence, specialist map)."""
+from __future__ import annotations
+
+import json
+import os
+import platform
+import re
+import shutil
+import subprocess
+import threading
+
+from oracle_release_runtime_w548 import OracleReleaseRuntimeW548 as _Base
+
+PROVED = __import__("os").environ.get("ORACLE7_ROOT", "/opt/oracle-clm") + "/specialists/_proved"
+NUM = re.compile(r"(? dict:
+ r = super().chat(text, session_id)
+ if not isinstance(r, dict) or r.get("status") != "answered":
+ return r
+ path = str(r.get("path", ""))
+ ids = SPEC.findall(path)
+ if len(set(ids)) != 1 or path.startswith("network"):
+ return r # one named specialist only; chained network answers are not checked here
+ k = _ask(ids[0], text)
+ if not k or k.get("status") != "answered" or not k.get("response"):
+ return r
+ prov = k.get("provenance") or {}
+ if agrees(r.get("response", ""), k.get("response", "")):
+ out = dict(r)
+ out["path"] = path + ">proved:" + str(prov.get("session", "veri7vn"))
+ out["proved"] = {"kernel": prov.get("kernel"), "session": prov.get("session"), "theorems": prov.get("theorems", []),
+ "source": "veri7vn (Isabelle/HOL, CC0, ember arlynx)"}
+ return out
+ out = dict(r)
+ out.update(status="withheld", path="proved_check:disagree>" + path,
+ response="A proved check (a veri7vn kernel, verified in Isabelle/HOL) gave a different answer to this question, so I won't give either as the answer.")
+ return out
diff --git a/root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w554.py b/root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w554.py
new file mode 100644
index 0000000000000000000000000000000000000000..b11c3e363c6149c70bb5e0ff67b13215fd087f21
--- /dev/null
+++ b/root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w554.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python3
+"""oracle_release_runtime_w554 - CAPABILITY RELEASE (3.2, with 3.3's work): THE STAR SPECIALISTS, THEN OWNER-FIRST.
+
+w552 and w553 in one fold (submitted together to save a gate cycle; their code is unchanged and runs in the same order, so
+the turn behaves exactly as the w551 -> w552 -> w553 chain whose regression, route and false-yes checks were run):
+ 1. STAR SPECIALISTS (w552): when the parent turn did not answer, the four star specialists admitted on 2026-10-18
+ (#214 stars, #215 habzone, #216 exoorbits, #217 transits; standing_stars_v1.txt) are asked through the release's own
+ combiner; a reply is kept only when exactly one answered it ("rule": "only") and did not decline.
+ 2. OWNER-FIRST (w553): when the turn is still not answered, the whole standing family plus the admitted specialists no list
+ carried (standing_owner_v1.txt: #18 orbits) is asked; an answer is kept only when one specialist gave it, it is not the
+ specialist the parent already stopped, EVERY number the question gives moves it (counterfactual dependence - the test
+ that refuses the astronomy reader's "29.8 km/s" for a satellite 400 km above Earth), and, if a number was asked, it gives
+ one that is not merely one of the question's own.
+Found by the research record (re-examination E-20261018-0043, together vs alone E-20261018-0033: answers lost when the
+wrong specialist takes a question; #18 orbits admitted but in no standing list). Deliberate declines of a question that is
+itself the problem (private, impossible, future, fictional, a false premise, an unknown place or body, half an answer) stand.
+The fold imports no runtime other than its parent and changes no module state of any other fold."""
+from __future__ import annotations
+
+import importlib.util
+import re
+import threading
+
+from oracle_release_runtime_w551 import OracleReleaseRuntimeW551 as _Base
+
+SPEC_ROOT = __import__("os").environ.get("ORACLE7_ROOT", "/opt/oracle-clm") + "/specialists"
+GATE = SPEC_ROOT + "/_gate"
+FAMILY_CODE = GATE + "/combo_runtime_v1.py"
+STAR_STANDING = GATE + "/standing_stars_v1.txt"
+LISTS = ["standing_v2", "standing_new_v1", "standing_new_v2", "standing_new_v3", "standing_new_v4", "standing_new_v5", "standing_stars_v1",
+ "standing_owner_v1"]
+STARS_STAND = ("private", "impossible", "future", "fiction", "premise", "unknown_place", "other_body", "half_answer")
+OWNER_STANDS = STARS_STAND + ("identity", "self", "act:")
+SPEC_IN_PATH = re.compile(r"\bspecialist:([a-z0-9][a-z0-9-]*)")
+NUM = re.compile(r"-?\d[\d,]*(?:\.\d+)?")
+NUMQ = re.compile(r"^\s*(how many|how much|how long|how far|how fast|how old|convert|calculate|compute|what is the)\b|\bin [a-z]+\s*\?\s*$", re.I)
+_S: dict = {}
+_LOCK = threading.Lock()
+
+
+def _load(key, lists):
+ with _LOCK:
+ if key not in _S:
+ spec = importlib.util.spec_from_file_location("o7_combo_runtime_v1_w554_" + key, FAMILY_CODE)
+ mod = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(mod)
+ ids = []
+ for f in lists:
+ for s in open("%s/%s.txt" % (GATE, f), encoding="utf-8").read().split():
+ ids += [m for m in s.split("+") if m not in ids]
+ _S[key] = (mod.Family(["%s/%s/manifest.json" % (SPEC_ROOT, i) for i in ids], ["%s/%s" % (SPEC_ROOT, i) for i in ids]), mod)
+ return _S[key]
+
+
+def _nums(s):
+ out = []
+ for m in NUM.finditer(str(s or "")):
+ try:
+ out.append(float(m.group(0).replace(",", "")))
+ except ValueError:
+ pass
+ return out
+
+
+class OracleReleaseRuntimeW554(_Base):
+ def _stars(self, text, session_id, r):
+ """w552, unchanged"""
+ low = str(r.get("path", "")).lower()
+ if any(k in low for k in STARS_STAND):
+ return r
+ try:
+ f = _load("stars", ["standing_stars_v1"])[0].reply(text, "w552" + str(session_id)[:30])
+ except Exception: # noqa: BLE001 - a specialist failure never breaks the turn; the parent's reply stands
+ return r
+ if not f or f.get("status") != "answered":
+ return r
+ prov = f.get("provenance") or {}
+ if (prov.get("trace") or {}).get("rule") != "only" or prov.get("declined"):
+ return r
+ out = dict(r)
+ out.update(status="answered", response=str(f.get("response", "")) + " [%s]" % prov.get("specialist", ""),
+ path="specialist_call>" + str(f.get("path", "")), references=f.get("references", []))
+ out.pop("why", None)
+ return out
+
+ def _owner(self, text, session_id, r):
+ """w553, unchanged"""
+ path = str(r.get("path", ""))
+ if any(k in path.lower() for k in OWNER_STANDS):
+ return r
+ consulted = set(SPEC_IN_PATH.findall(path))
+ try:
+ fam, mod = _load("owner", LISTS)
+ with _LOCK:
+ chosen, trace = fam._one(text, "w553" + str(session_id)[:30])
+ if chosen is None or chosen.get("declined") or chosen.get("status") != "answered":
+ return r
+ sid = chosen.get("id") or getattr(chosen.get("_organ"), "id", None)
+ if not sid or sid in consulted:
+ return r
+ givens = mod._givens(text)
+ if givens and fam._dependence(chosen, text, givens) < len(givens):
+ return r
+ except Exception: # noqa: BLE001 - a specialist failure never breaks the turn; the parent's reply stands
+ return r
+ resp = str(chosen.get("response", ""))
+ if NUMQ.search(text):
+ given = _nums(text)
+ if not [v for v in _nums(resp) if not any(abs(v - g) <= 1e-9 * max(1.0, abs(g)) for g in given)]:
+ return r
+ out = dict(r)
+ out.update(status="answered", response=resp + " [%s]" % sid, references=chosen.get("references", []),
+ path="owner_first>specialist_call>specialist:%s>%s" % (sid, chosen.get("kind", "")))
+ out.pop("why", None)
+ return out
+
+ def chat(self, text: str, session_id: str = "default") -> dict:
+ r = super().chat(text, session_id)
+ if not isinstance(r, dict) or r.get("status") == "answered":
+ return r
+ r = self._stars(text, session_id, r)
+ if r.get("status") == "answered":
+ return r
+ return self._owner(text, session_id, r)
diff --git a/root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w555.py b/root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w555.py
new file mode 100644
index 0000000000000000000000000000000000000000..69a33e998d539429abf30a43053e1d698f981e38
--- /dev/null
+++ b/root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w555.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+"""oracle_release_runtime_w555 - CORRECTNESS RELEASE (3.2): OWNER-FIRST NEVER OVERRULES A QUESTION THAT IS ITSELF THE PROBLEM.
+
+Found by the release-candidate smoke (smoke_compose_reloc_v3, probe "zentar", the published miss of sealed test 20) on
+2026-10-18, before sealed test 23 ran: asked "How many minutes are in a day on the planet Zentar?", the head rightly withholds
+(precision_guard:Q5_unknown_body - there is no planet Zentar), and w554's owner-first step then answered "1 day is 1440
+minutes" from the units specialist; the same for Tralfamadore. Its list of declines that must stand named "unknown_place"
+and "other_body" but not "unknown_body".
+
+Also found then: asked "What is the orbital period of a circular orbit at 200 km altitude around Earth?", the astronomy
+reader answers Earth's own orbital period (365.2 days) - and the proved cross-check marks it, because the kernel reads the
+question the same way (veri7vn proves the computation; its recognizer, which reads the question, is not proved). An
+astronomy answer to a question that names an orbit's altitude is now withheld as the wrong reading, and owner-first may
+answer it from #18 orbits under its numbers test.
+
+Narrowing otherwise. Owner-first (w554 step 2) no longer acts when the head's withholding is any of the precision guard's Q rules
+- the rules that say the QUESTION is the problem: an unknown body or place, half an answer, a wrong gravity, paths that
+disagree, not physical, a pronoun or name missing, a bare numeral. Every other turn is w554's, byte for byte; the star
+specialists (w554 step 1) are unchanged. The fold imports no runtime other than its parent and changes no module state of any
+other fold."""
+from __future__ import annotations
+
+import re
+
+from oracle_release_runtime_w554 import OracleReleaseRuntimeW554 as _Base
+
+
+def _guard_rule(path):
+ p = str(path or "")
+ if not p.startswith("precision_guard:"):
+ return None
+ return p.split(":", 1)[1].split(">")[0].split("|")[0]
+
+
+ALTITUDE = re.compile(r"\b\d[\d,]*(?:\.\d+)?\s*(?:km|kilomet(?:er|re)s?|miles?)\s+(?:altitude|above|high)\b|\baltitude of\b|\bcircular orbit\b", re.I)
+
+
+class OracleReleaseRuntimeW555(_Base):
+ def _owner(self, text, session_id, r):
+ rule = _guard_rule(r.get("path", ""))
+ if rule is not None and rule[:1] == "Q":
+ return r # the question is the problem: the head's decline stands
+ return super()._owner(text, session_id, r)
+
+ def chat(self, text: str, session_id: str = "default") -> dict:
+ r = super().chat(text, session_id)
+ # the astronomy readers answer a planet's own figures ("Earth's orbital period is 365.2 Earth days"); a question that
+ # names an orbit's altitude asks about a spacecraft's orbit around that body, which #18 orbits computes. The planet
+ # figure is withheld as the wrong reading, and owner-first may ask the family (its numbers test applies).
+ if isinstance(r, dict) and r.get("status") == "answered" and "specialist:astronomy" in str(r.get("path", "")) and ALTITUDE.search(text):
+ held = dict(r, status="withheld", path="reader_guard:planet_figure_for_an_orbit>" + str(r.get("path", "")),
+ response="That asks about an orbit at a given altitude, and what I found is the planet's own figure, so I won't give it as the answer.")
+ held.pop("proved", None)
+ out = self._owner(text, session_id, held)
+ return out
+ return r
diff --git a/root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w556.py b/root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w556.py
new file mode 100644
index 0000000000000000000000000000000000000000..5e4a48d615588115b36cc3808c9ce51ad516f9b5
--- /dev/null
+++ b/root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w556.py
@@ -0,0 +1,148 @@
+#!/usr/bin/env python3
+"""oracle_release_runtime_w556 - CORRECTNESS RELEASE (3.2 full): THREE GUARDS ON ANSWERS THAT HAVE NOTHING UNDER THEM.
+
+Found by sealed test 23 on 2026-10-18, which the full 3.2 failed with 3 false yeses against a bar of 2 (slim passed with 1).
+Each guard is a class of error, not an item; sealed 23 is spent and is not the test of this fold.
+
+ 1. NO STORED FACT WITHOUT GIVENS. w554's owner-first keeps a specialist's answer only when every number the question gives
+ moves it. A question that gives no numbers passed that test vacuously: "How old is the president of Antarctica?" was
+ answered with the US constitution's minimum age (a stored fact, kind fact_number). With no givens, owner-first may no
+ longer hand back a stored fact; computations and a named body's constants still answer (requiring givens for every
+ answer was measured first and lost three right escape-velocity answers).
+ 2. A NEGATIVE AMOUNT OF SOMETHING THAT CANNOT BE NEGATIVE. "A 100 W bulb runs for -5 hours" was answered 0.5 kWh, the sign
+ dropped (slim and full). A question that gives a negative duration, power, energy, mass, volume or count is withheld
+ on every route, as the precision guard withholds other questions that are themselves the problem (a Q rule, so
+ owner-first never overrules it).
+ 3. (MEASURED AND REMOVED) Evidence must name what the question names, aimed at "Who directed City of God?" (answered from a
+ sentence about another work) and "In what year did Napoleon visit Neptune?" (answered 1989 from a sentence about
+ Voyager 2). Every form of the check measured on the capability panel lost right answers - all names 62, then 21 on the
+ fact routes, any name 7 - because relation answers cite the answer's own page, which need not repeat the question's
+ names. It is not in this fold; those two sealed-23 failures stay open: disclosed.
+ 4. ARITHMETIC IS NOT READ FROM A SENTENCE. "A walker covers 5 km in 60 minutes ... how many minutes does 8 km take?" was
+ answered 50 from an unrelated sentence. A question that gives two or more numbers and asks how many/much/long is not
+ answered from retrieved text; the specialist network (v6, learned operations) is asked instead, and if it has no
+ checked answer the turn is withheld.
+
+Every other turn is w555's, byte for byte (guards 1, 2 and 4 only). The fold imports no runtime other than its parent; the network is loaded from
+specialists/_gate under its own module name, as w550 does, and a network failure never costs the parent's reply."""
+from __future__ import annotations
+
+import importlib.util
+import re
+import sys
+import threading
+
+from oracle_release_runtime_w423 import OracleReleaseRuntimeW423 as _Base
+
+SPEC_ROOT = __import__("os").environ.get("ORACLE7_ROOT", "/opt/oracle-clm") + "/specialists"
+GATE = SPEC_ROOT + "/_gate"
+NET6_CODE = GATE + "/network_v6.py"
+NET_STANDING = GATE + "/standing_net_v6.txt"
+
+NUM = re.compile(r"\d[\d,]*(?:\.\d+)?")
+NONNEG = (r"hours?|hrs?|minutes?|mins?|seconds?|secs?|days?|weeks?|months?|years?|watts?|W|kW|MW|kilowatts?|kWh|MWh|Wh|joules?|J|"
+ r"kg|kilograms?|grams?|g|mg|pounds?|lbs?|tonnes?|tons?|litres?|liters?|L|mL|millilit(?:er|re)s?|gallons?|"
+ r"people|persons?|students?|workers?|apples?|books?|items?|cars?|boxes|eggs?|cups?|bottles?")
+NEGQ = re.compile(r"(?:(? 1]
+ if not words:
+ return True
+ w = words[-1]
+ stem = w if len(w) <= 4 else w[:max(4, len(w) - 3)]
+ return re.search(r"\b%s" % re.escape(stem), text.lower()) is not None
+
+
+def _withheld(r, path, response):
+ out = dict(r)
+ out.update(status="withheld", path=path + ">" + str(r.get("path", "")), response=response)
+ for k in ("proved", "references"):
+ out.pop(k, None)
+ return out
+
+
+class OracleReleaseRuntimeW556(_Base):
+ def _owner(self, text, session_id, r):
+ out = super()._owner(text, session_id, r)
+ # guard 1: with no givens the numbers test is vacuous, so owner-first may not hand back a STORED FACT read off
+ # (a fact_* kind: "A President must be at least 35" for the president of Antarctica). A computation or a body's
+ # physical constant (escape velocity of Mars, gravity of Jupiter) still answers - measured: requiring givens for
+ # every answer lost three right escape-velocity answers on the capability panel.
+ if (out is not r and isinstance(out, dict) and not NUM.search(str(text or ""))
+ and re.search(r">fact[^>]*$", str(out.get("path", "")))):
+ return r
+ return out
+
+ def chat(self, text: str, session_id: str = "default") -> dict:
+ r = super().chat(text, session_id)
+ if not isinstance(r, dict) or r.get("status") != "answered":
+ return r
+ q = str(text or "")
+ resp = str(r.get("response", ""))
+ neg = NEGQ.search(q)
+ if neg: # guard 2
+ return _withheld(r, "precision_guard:Q9_negative_amount",
+ "The question gives %s, a negative amount of something that cannot be negative, so I won't compute an answer from it." % neg.group(0).strip())
+ if not resp.startswith("[ev:") or not str(r.get("path", "")).startswith(FACT_ROUTES):
+ return r # guards 3 and 4 judge single facts read from evidence, nothing else
+ if len(NUM.findall(q)) >= 2 and HOWQ.search(q): # guard 4
+ try:
+ n = _net().solve(q)
+ except Exception: # noqa: BLE001
+ n = {}
+ if n.get("status") == "answered":
+ out = dict(r)
+ hops = [str(t.get("node")) for t in n.get("trace") or []]
+ out.update(status="answered", path="network6>" + " ".join(hops), response=n["response"], references=[],
+ provenance={"kind": "specialist_network_v6", "trace": n.get("trace"), "core_path": r.get("path")})
+ out.pop("proved", None)
+ return out
+ return _withheld(r, "evidence_guard:arithmetic_from_text",
+ "That needs a calculation from the numbers you gave, and what I found is a sentence, not a calculation, so I won't give it as the answer.")
+ # guard 3 (evidence must name what the question names) was measured three ways and removed: requiring every name
+ # lost 62, then 21 right answers on the capability panel; requiring any name still lost 7 (relation answers cite the
+ # answer's own page, which need not repeat the question's names). City of God and Napoleon-Neptune stay unfixed.
+ return r
diff --git a/root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w557.py b/root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w557.py
new file mode 100644
index 0000000000000000000000000000000000000000..0cf9608118f1d57c386a180d0e4d61b393eba139
--- /dev/null
+++ b/root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w557.py
@@ -0,0 +1,303 @@
+#!/usr/bin/env python3
+"""oracle_release_runtime_w557 - ORACLE7 4.0 THE LAW (built from Ben's invocation; build brief: Claude Doc "Oracle7 V4.0 - Build
+Brief"). Submitted as a CORRECTNESS RELEASE: by design it adds no capability the gate's columns can see (its claims are tested
+on the two sealed V4 panels), so it is exempt from no_gain only; every measured column must still hold.
+
+Three parts, one fold, so the law is admitted whole:
+
+1. THE INSTRUMENT (I4, and the logs for I3, I5, I6). Every turn, BEFORE its reply is returned, appends one entry to the residue
+ ledger: k = the Tree edges its path walks; L = wall-clock ms; word = the path spelled over LAMBDA, the twenty-two paths of
+ the three-tree runtime (oracle_tritree_v1.TREE_OF_LIFE) - a logged operator sequence is rejoined by the shortest edge path
+ between operator nodes, since the runtime passes through a node whose operator does not apply without logging it (its
+ TRANSIT rule); routes that do not walk the Tree are spelled by ROUTE_SPELLING (measured = false, an interpretation recorded
+ as such); rho = for a turn that did not answer, the node where formation stopped, the prefix reached and the reason.
+ Append-only and hash-chained. PRIVACY: the question is stored as its sha256 only; its text only when the operator sets
+ ORACLE7_V4_LEDGER_TEXT=1 (research box, gate panels; never the public demo). Ledger: $ORACLE7_V4_LEDGER, else
+ $ORACLE7_STATE/v4_ledger.jsonl, else /tmp/oracle7-v4/v4_ledger.jsonl. A failed write never costs the turn.
+
+2. NON-IDENTITY (I1, from P1: "never has become ... it becomes"). An ANSWERED reply that has nothing under it - no citation,
+ no proof, no checked computation, no specialist - does not become conversation state: the session's state (dialogue,
+ transitions, pending conversions) is put back as it was before the turn, and the reply is entered in the ledger as a
+ discourse record. A later turn can then use it only by re-entering it as cited evidence. Declined and withheld turns keep
+ their state (there is no answer in them to carry, and "why not?" must still explain the decline); acts, which are
+ conversions over the previous answer, are untouched.
+
+3. WALK RECALL (P3, from "go faster and faster, but slower in states"). The three-tree runtime's answer(q) is a pure function
+ of the question and the stores, holding no session state. A walk already made is recalled, not walked again: the same
+ result, the same path, the same step count, less wall-clock. Bounded (2,048 walks per instance), per process, on every
+ TriTree instance the chain has made (an instance attribute; no module state of any fold is changed). And a specialist's
+ own-module test (combo_runtime_v1._Organ._own, about a quarter of a relation turn's time: the family rescans every
+ loaded module after a call, one abspath each) is recalled per file on each loaded specialist - the same answer, never
+ recomputed. The frozen files are untouched; only instances get the recall.
+
+Every reply is the parent's (w556) except where part 2 restores state; part 1 and part 3 change no reply."""
+from __future__ import annotations
+
+import copy
+import hashlib
+import json
+import os
+import re
+import threading
+import time
+from collections import OrderedDict, deque
+
+from oracle_release_runtime_w556 import OracleReleaseRuntimeW556 as _Base
+
+SEPH = ["keter", "chokmah", "binah", "chesed", "geburah", "tiferet", "netzach", "hod", "yesod", "malkuth"]
+EDGES = [("keter", "chokmah"), ("keter", "binah"), ("keter", "tiferet"), ("chokmah", "binah"), ("chokmah", "tiferet"),
+ ("chokmah", "chesed"), ("binah", "tiferet"), ("binah", "geburah"), ("chesed", "geburah"),
+ ("chesed", "tiferet"), ("chesed", "netzach"), ("geburah", "tiferet"), ("geburah", "hod"),
+ ("tiferet", "netzach"), ("tiferet", "yesod"), ("tiferet", "hod"), ("netzach", "hod"), ("netzach", "yesod"),
+ ("netzach", "malkuth"), ("hod", "yesod"), ("hod", "malkuth"), ("yesod", "malkuth")]
+NODE_OF = {"READ": "malkuth", "FRAME": "yesod", "LINK": "chokmah", "RELATE": "binah", "EXPAND": "chesed", "CHECK": "geburah",
+ "BIND": "tiferet", "COMPARE": "netzach", "COMPUTE": "hod", "VERIFY": "keter"}
+OPS = re.compile(r"(?|$)")
+ROUTE_SPELLING = [
+ ("precision_guard:Q", ["READ", "FRAME", "CHECK"]), ("precision_guard", ["READ", "FRAME", "LINK", "CHECK"]),
+ ("evidence_guard", ["READ", "FRAME", "LINK", "RELATE", "CHECK"]), ("reader_guard", ["READ", "FRAME", "LINK", "CHECK"]),
+ ("owner_first", ["READ", "FRAME", "LINK", "COMPUTE", "CHECK", "BIND", "VERIFY"]),
+ ("network", ["READ", "FRAME", "COMPUTE", "BIND", "CHECK", "VERIFY"]),
+ ("specialist_call", ["READ", "FRAME", "COMPUTE", "BIND", "VERIFY"]), ("specialist_after_guard", ["READ", "FRAME", "COMPUTE", "BIND", "VERIFY"]),
+ ("family_precedence", ["READ", "FRAME", "LINK", "COMPUTE", "BIND", "VERIFY"]),
+ ("compute:", ["READ", "FRAME", "COMPUTE", "CHECK", "VERIFY"]), ("organ:math", ["READ", "FRAME", "COMPUTE", "CHECK", "VERIFY"]),
+ ("op:", ["READ", "FRAME", "COMPUTE", "VERIFY"]), ("verified:", ["READ", "FRAME", "LINK", "RELATE", "CHECK", "VERIFY"]),
+ ("factoid", ["READ", "FRAME", "LINK", "RELATE", "CHECK"]), ("unverified", ["READ", "FRAME", "LINK", "RELATE", "CHECK"]),
+ ("capability:", ["READ", "FRAME", "LINK", "EXPAND", "BIND", "VERIFY"]), ("arb:", ["READ", "FRAME", "LINK", "EXPAND", "BIND"]),
+ ("act:", ["READ", "FRAME", "BIND"]), ("chat:", ["READ", "FRAME"]),
+]
+# what an answer may stand on and still become state: a citation or source tag, a proof, or a checked route
+CITED = re.compile(r"\[[^\]\s]{2,}\]")
+CHECKED = ("specialist_call", "specialist_after_guard", "owner_first", "family_precedence", "network", "compute:", "organ:math",
+ "op:", "verified:", "algebra:", "deduce:")
+SESSION_STORES = ("dialogue", "transitions", "_pending_conversion")
+_ADJ = {s: [] for s in SEPH}
+for _i, (_a, _b) in enumerate(EDGES):
+ _ADJ[_a].append((_b, _i))
+ _ADJ[_b].append((_a, _i))
+_LOCK = threading.Lock()
+_S: dict = {}
+
+
+def shortest(a, b):
+ """the shortest edge path a -> b as [(node, edge index)], ties broken by edge order (deterministic)"""
+ if a == b:
+ return []
+ prev, q = {a: None}, deque([a])
+ while q:
+ u = q.popleft()
+ for v, e in sorted(_ADJ[u], key=lambda x: x[1]):
+ if v not in prev:
+ prev[v] = (u, e)
+ if v == b:
+ q.clear()
+ break
+ q.append(v)
+ out, n = [], b
+ while prev[n] is not None:
+ u, e = prev[n]
+ out.append((n, e))
+ n = u
+ return out[::-1]
+
+
+def ops_of(path):
+ """(operators in order, measured) - the Tree operators a path logged, or the route's spelling"""
+ p = str(path or "")
+ ops = [(m.group(1) or m.group(2)).upper() for m in OPS.finditer(p)]
+ if len(ops) >= 2:
+ return ops, True
+ for prefix, spelled in ROUTE_SPELLING:
+ if p.startswith(prefix):
+ return list(spelled), False
+ return ["READ", "FRAME"], False
+
+
+def spell(ops):
+ """(nodes walked, LAMBDA word as edge indices): from Malkuth through each operator's node by shortest edge paths"""
+ nodes, word, cur = ["malkuth"], [], "malkuth"
+ for op in ops:
+ tgt = NODE_OF.get(op)
+ if not tgt or (tgt == cur and op == "READ"):
+ continue
+ for n, e in shortest(cur, tgt):
+ nodes.append(n)
+ word.append(e)
+ cur = tgt
+ return nodes, word
+
+
+def stands_on_something(r):
+ if r.get("proved"):
+ return True
+ path = str(r.get("path", ""))
+ return path.startswith(CHECKED) or bool(CITED.search(str(r.get("response", ""))))
+
+
+def _ledger_path():
+ p = os.environ.get("ORACLE7_V4_LEDGER")
+ if p:
+ return p
+ return os.path.join(os.environ.get("ORACLE7_STATE") or "/tmp/oracle7-v4", "v4_ledger.jsonl")
+
+
+def ledger_append(entry):
+ with _LOCK:
+ p = _ledger_path()
+ if _S.get("path") != p:
+ _S["path"], _S["prev"] = p, "0" * 64
+ try:
+ with open(p, "rb") as f:
+ f.seek(0, 2)
+ f.seek(max(0, f.tell() - 65536))
+ last = f.read().splitlines()[-1]
+ _S["prev"] = json.loads(last)["h"]
+ except Exception: # noqa: BLE001 - a new or unreadable ledger starts a new chain
+ pass
+ entry["prev"] = _S["prev"]
+ entry["h"] = hashlib.sha256(json.dumps(entry, sort_keys=True, ensure_ascii=False).encode()).hexdigest()
+ os.makedirs(os.path.dirname(p) or ".", exist_ok=True)
+ with open(p, "a", encoding="utf-8") as f:
+ f.write(json.dumps(entry, sort_keys=True, ensure_ascii=False) + "\n")
+ _S["prev"] = entry["h"]
+
+
+def ledger_entry(text, session_id, r, L, law, extra=None):
+ path = str(r.get("path", "")) if isinstance(r, dict) else ""
+ status = r.get("status") if isinstance(r, dict) else None
+ ops, measured = ops_of(path)
+ nodes, word = spell(ops)
+ e = {"t": round(time.time(), 3), "law": law, "session": hashlib.sha256(str(session_id).encode()).hexdigest()[:16],
+ "input": hashlib.sha256(str(text or "").encode()).hexdigest(), "status": status, "route": path.split(">")[0][:60],
+ "measured": measured, "ops": ops, "nodes": nodes, "word": word, "k": len(word), "L_ms": L}
+ if os.environ.get("ORACLE7_V4_LEDGER_TEXT") == "1":
+ e["text"] = str(text or "")[:500]
+ if status != "answered":
+ e["rho"] = {"stopped_at": nodes[-1], "prefix": word, "reason": path.split(">")[0][:80] or str(status)}
+ if extra:
+ e.update(extra)
+ return e
+
+
+class _WalkRecall:
+ """a bounded memo over one TriTree instance's answer(q, form_props, only): same result, its why and expanded restored"""
+
+ def __init__(self, tt, size=2048):
+ self.tt, self.fn, self.size, self.memo, self.hits, self.lock = tt, tt.answer, size, OrderedDict(), 0, threading.Lock()
+
+ def __call__(self, q, form_props=None, only=False):
+ key = (str(q), tuple(sorted(form_props)) if form_props else None, bool(only))
+ with self.lock:
+ if key in self.memo:
+ self.memo.move_to_end(key)
+ res, why, expanded = self.memo[key]
+ self.hits += 1
+ self.tt.why, self.tt.expanded = why, expanded
+ return copy.deepcopy(res)
+ res = self.fn(q, form_props, only) if only else self.fn(q, form_props)
+ with self.lock:
+ self.memo[key] = (copy.deepcopy(res), getattr(self.tt, "why", None), getattr(self.tt, "expanded", None))
+ if len(self.memo) > self.size:
+ self.memo.popitem(last=False)
+ return res
+
+
+class _OwnRecall:
+ """a specialist's _own(mod) - does this module's file live in my dir? - recalled per file instead of recomputed: the
+ family's _env rescans every loaded module after a call that imported anything, and the answer for a file never changes"""
+
+ def __init__(self, organ):
+ self.prefix, self.memo = organ.dir + os.sep, {}
+
+ def __call__(self, mod):
+ f = getattr(mod, "__file__", None) or ""
+ if not f:
+ return False
+ r = self.memo.get(f)
+ if r is None:
+ r = self.memo[f] = os.path.abspath(f).startswith(self.prefix)
+ return r
+
+
+def recall_organs():
+ """give every loaded specialist (combo_runtime _Organ) its _own recall, once (an instance attribute; files unchanged)"""
+ import gc
+ n = 0
+ for o in gc.get_objects():
+ try:
+ if type(o).__name__ == "_Organ" and isinstance(getattr(o, "dir", None), str) and "_own" not in vars(o):
+ o._own = _OwnRecall(o)
+ n += 1
+ except Exception: # noqa: BLE001
+ continue
+ return n
+
+
+class OracleReleaseRuntimeW557(_Base):
+ LAW = "4.0"
+ SCAN_AT = {1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144}
+
+ def _v4_recall(self):
+ """wrap every three-tree instance the chain has made so far (folds build theirs lazily, on first use)"""
+ rcs = getattr(self, "_v4_rcs", None)
+ if rcs is None:
+ rcs = self._v4_rcs = []
+ if os.environ.get("ORACLE7_V4_RECALL") == "0": # the I3 dose-response runs one throughput setting without recall
+ return rcs
+ try:
+ for v in list(vars(self).values()):
+ for o in (v if isinstance(v, (tuple, list)) else (v,)):
+ if (hasattr(o, "adj") and hasattr(o, "max_states") and callable(getattr(o, "answer", None))
+ and not isinstance(o.answer, _WalkRecall)):
+ rc = _WalkRecall(o)
+ o.answer = rc
+ rcs.append(rc)
+ except Exception: # noqa: BLE001 - without its recall the fold walks every time
+ pass
+ return rcs
+
+ def _v4_snapshot(self, session_id):
+ snap = {}
+ for name in SESSION_STORES:
+ store = getattr(self, name, None)
+ if isinstance(store, dict):
+ snap[name] = (session_id in store, copy.deepcopy(store.get(session_id)))
+ return snap
+
+ def _v4_restore(self, session_id, snap):
+ for name, (had, val) in snap.items():
+ store = getattr(self, name, None)
+ if not isinstance(store, dict):
+ continue
+ if had:
+ store[session_id] = val
+ else:
+ store.pop(session_id, None)
+
+ def chat(self, text: str, session_id: str = "default") -> dict:
+ hits0 = sum(rc.hits for rc in self._v4_recall())
+ try:
+ snap = self._v4_snapshot(session_id)
+ except Exception: # noqa: BLE001
+ snap = None
+ t0 = time.perf_counter()
+ r = super().chat(text, session_id)
+ L = round(1000.0 * (time.perf_counter() - t0), 1)
+ extra = {}
+ try:
+ if (isinstance(r, dict) and r.get("status") == "answered" and snap is not None
+ and not str(r.get("path", "")).startswith("act:") and not stands_on_something(r)):
+ self._v4_restore(session_id, snap)
+ extra["discourse_record"] = True
+ hits = sum(rc.hits for rc in self._v4_recall()) # also wraps instances this turn created
+ if hits > hits0:
+ extra["recalled_walks"] = hits - hits0
+ self._v4_n = getattr(self, "_v4_n", 0) + 1
+ if os.environ.get("ORACLE7_V4_RECALL") != "0" and (self._v4_n in self.SCAN_AT or self._v4_n % 233 == 0):
+ # specialist families load lazily: look again, more rarely
+ recall_organs()
+ ledger_append(ledger_entry(text, session_id, r, L, self.LAW, extra))
+ except Exception: # noqa: BLE001 - the law's bookkeeping never costs the turn
+ pass
+ return r
diff --git a/root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w558.py b/root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w558.py
new file mode 100644
index 0000000000000000000000000000000000000000..c28dce99b8d09726278d00ca769aea358f14518e
--- /dev/null
+++ b/root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/oracle_release_runtime_w558.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python3
+"""oracle_release_runtime_w558 - ORACLE7 4.0, PRONOUN RE-ENTRY (Ben 2026-10-19: "add pronouns to 4.0"). A CORRECTNESS RELEASE
+in the sense the gate uses: it may not lower any column; its effect is measured on the second sealed V4 panel's follow-ups.
+
+4.0's law (w557, I1): a past answer does not become state; a later turn may use it only by RE-ENTERING it as cited evidence.
+3.2 already resolves many pronoun follow-ups ("When was she born?" after "Who wrote Emma?" -> 16 December 1775). Where it
+cannot - the resolved question does not parse ("What is its capital?" after "Which country is Kyoto in?" was withheld as
+subject-missing; "What else did he direct?" after "Who directed Vertigo?" asked to clarify) - this fold re-enters the
+previous answer itself: the pronoun is replaced by that answer and the question is asked once more ("What is Japan's
+capital?"). Conditions, all required:
+ - the session's previous answer stood on something (a citation, a proof, a checked route: w557's stands_on_something)
+ and named one thing: a person after a "who" question (he/she/him/her/his), a place or thing after a "which/what
+ " question (it/its);
+ - the new turn was NOT answered by the chain, is short (<= 14 words), names nothing of its own (no capitalised word after
+ the first) and carries a pronoun of the right kind;
+ - the re-entered question is answered; otherwise the chain's own reply stands.
+The reply says what was re-entered ("taking 'its' as Japan, from my previous answer"). The ledger keeps one entry per turn:
+this fold repeats w557's bookkeeping around its own turn instead of calling w557's chat twice."""
+from __future__ import annotations
+
+import re
+import time
+
+from oracle_release_runtime_w557 import OracleReleaseRuntimeW557 as _Base
+from oracle_release_runtime_w557 import ledger_append, ledger_entry, stands_on_something, recall_organs
+
+PERSON_Q = re.compile(r"^\s*who\b", re.I)
+THING_Q = re.compile(r"^\s*(?:which|what)\s+(country|city|town|state|province|island|planet|moon|river|mountain|lake|sea|ocean|"
+ r"continent|region|book|novel|film|movie|play|opera|painting|team|club|company|band|album|song)\b", re.I)
+PERSON_P = re.compile(r"\b(he|she|him|her|his|hers)\b", re.I)
+THING_P = re.compile(r"\b(it|its)\b", re.I)
+TAG = re.compile(r"^\s*(?:\[[^\]]+\]\s*)+")
+NOT_POSS = {"in", "on", "at", "to", "for", "with", "from", "by", "and", "or", "about", "as", "of", "?"}
+
+
+def answer_entity(reply):
+ """the one thing a reply names first: 'Jane Austen' from 'Jane Austen. From the Wikidata relation store: ...'"""
+ head = TAG.sub("", str(reply or "")).split(". ", 1)[0].strip().rstrip(".")
+ words = head.split()
+ if not words or len(words) > 6 or re.search(r"\d", head) or not words[0][:1].isupper():
+ return None
+ return head
+
+
+def substitute(text, entity, kind):
+ rx = PERSON_P if kind == "person" else THING_P
+ toks = re.split(r"(\W+)", text)
+ out, used = [], False
+ for i, t in enumerate(toks):
+ low = t.lower()
+ if rx.fullmatch(t or "") and low:
+ nxt = next((x for x in toks[i + 1:] if x.strip()), "?").lower()
+ poss = low in ("his", "its", "hers") or (low == "her" and nxt not in NOT_POSS and nxt.isalpha())
+ out.append(entity + ("'s" if poss else ""))
+ used = True
+ else:
+ out.append(t)
+ return "".join(out) if used else None
+
+
+class OracleReleaseRuntimeW558(_Base):
+ def _v4_turn(self, text, session_id):
+ """the chain's reply, or - where it could not resolve a pronoun - the previous cited answer re-entered"""
+ r = super(_Base, self).chat(text, session_id) # the chain below w557 (w556 and down), as w557 itself calls it
+ disc = getattr(self, "_v4_disc", None)
+ if disc is None:
+ disc = self._v4_disc = {}
+ q = str(text or "")
+ if isinstance(r, dict) and r.get("status") == "answered":
+ rec0 = disc.get(session_id)
+ kind = "person" if PERSON_Q.search(q) else ("thing" if THING_Q.search(q) else None)
+ if kind and stands_on_something(r):
+ ent = answer_entity(r.get("response"))
+ disc[session_id] = {"entity": ent, "kind": kind} if ent else None
+ elif not (rec0 and (PERSON_P if rec0["kind"] == "person" else THING_P).search(q)):
+ disc[session_id] = None # a new subject: the old answer is no longer what a pronoun means
+ return r # (a follow-up the chain resolved itself keeps the record)
+ rec = disc.get(session_id)
+ words = q.split()
+ if (not rec or len(words) > 14 or any(w[:1].isupper() for w in words[1:])
+ or not (PERSON_P if rec["kind"] == "person" else THING_P).search(q)):
+ return r
+ q2 = substitute(q, rec["entity"], rec["kind"])
+ if not q2:
+ return r
+ r2 = super(_Base, self).chat(q2, session_id)
+ if not (isinstance(r2, dict) and r2.get("status") == "answered"):
+ return r
+ pron = ((PERSON_P if rec["kind"] == "person" else THING_P).search(q).group(0))
+ out = dict(r2)
+ out["path"] = "reentry:cited>" + str(r2.get("path", ""))
+ out["response"] = "(Taking '%s' as %s, from my previous answer.) %s" % (pron, rec["entity"], r2.get("response", ""))
+ out["reentry"] = {"pronoun": pron, "entity": rec["entity"], "question": q2}
+ return out
+
+ def chat(self, text: str, session_id: str = "default") -> dict:
+ # w557's bookkeeping (snapshot, non-identity, recall counts, the ledger entry) around this fold's turn - once per turn
+ hits0 = sum(rc.hits for rc in self._v4_recall())
+ try:
+ snap = self._v4_snapshot(session_id)
+ except Exception: # noqa: BLE001
+ snap = None
+ t0 = time.perf_counter()
+ r = self._v4_turn(text, session_id)
+ L = round(1000.0 * (time.perf_counter() - t0), 1)
+ extra = {}
+ try:
+ if (isinstance(r, dict) and r.get("status") == "answered" and snap is not None
+ and not str(r.get("path", "")).startswith("act:") and not stands_on_something(r)):
+ self._v4_restore(session_id, snap)
+ extra["discourse_record"] = True
+ if isinstance(r, dict) and r.get("reentry"):
+ extra["reentry"] = r["reentry"]["entity"]
+ hits = sum(rc.hits for rc in self._v4_recall())
+ if hits > hits0:
+ extra["recalled_walks"] = hits - hits0
+ self._v4_n = getattr(self, "_v4_n", 0) + 1
+ import os as _os
+ if _os.environ.get("ORACLE7_V4_RECALL") != "0" and (self._v4_n in self.SCAN_AT or self._v4_n % 233 == 0):
+ recall_organs()
+ ledger_append(ledger_entry(text, session_id, r, L, self.LAW, extra))
+ except Exception: # noqa: BLE001
+ pass
+ return r
diff --git a/root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/v4_law.py b/root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/v4_law.py
new file mode 100644
index 0000000000000000000000000000000000000000..069c9558a9185f9a256ba215c1d833d1ae498026
--- /dev/null
+++ b/root/fable-content/oracle-m-series-2026-08-15/self_loop/admitted/v4_law.py
@@ -0,0 +1,142 @@
+#!/usr/bin/env python3
+"""v4_law - Oracle7 4.0: the path alphabet LAMBDA, path vectors, the Tree-of-Life stabilizer and the re-entrance tracker.
+
+LAMBDA = the twenty-two paths of the three-tree runtime (oracle_tritree_v1.TREE_OF_LIFE), letters 0..21 in that order.
+A turn's word is the sequence of edges its walk takes from Malkuth (written to the ledger by fold w557).
+
+PATH(w) = sum_p bind(K[w_p], R[p]) in the codebook construction of 3.2's path memory (specialists/_gate/path_hrr_v1:
+osub_seed = sha256-seeded unit complex phasors, D = 1024; roles R[p] = osub_seed("o7pos:%d" % p); letters
+K[e] = osub_seed("o7lam:%d" % e)).
+
+THE STABILIZER. Plain cleanup reads each position against every letter. The Tree-of-Life stabilizer reads position p only
+against the letters that leave the node position p-1 ended at (a walk can only continue along an edge of its current node),
+starting at Malkuth: 2-6 candidates instead of 22. Same vector, same codebook; only the cleanup is constrained by the Tree.
+
+RE-ENTRANCE (I6). A form is re-entered when a later turn's PATH has cosine >= TAU with it. TAU = 0.95, fixed here before any
+4.0 panel ran: a word of up to 19 letters re-entered with one letter different still scores below it, so re-entered means
+the same walk. Reported against a baseline of shuffled words (each word's letters permuted, the same lengths).
+
+usage: v4_law.py capacity [out.json] - exact-word recall of N bundled walks, plain vs stabilized
+ v4_law.py reentry - re-entrance of the ledger's forms, against the shuffled baseline"""
+from __future__ import annotations
+
+import hashlib
+import json
+import random
+import sys
+
+import numpy as np
+
+D = 1024
+TAU = 0.95
+SEPH = ["keter", "chokmah", "binah", "chesed", "geburah", "tiferet", "netzach", "hod", "yesod", "malkuth"]
+EDGES = [("keter", "chokmah"), ("keter", "binah"), ("keter", "tiferet"), ("chokmah", "binah"), ("chokmah", "tiferet"),
+ ("chokmah", "chesed"), ("binah", "tiferet"), ("binah", "geburah"), ("chesed", "geburah"),
+ ("chesed", "tiferet"), ("chesed", "netzach"), ("geburah", "tiferet"), ("geburah", "hod"),
+ ("tiferet", "netzach"), ("tiferet", "yesod"), ("tiferet", "hod"), ("netzach", "hod"), ("netzach", "yesod"),
+ ("netzach", "malkuth"), ("hod", "yesod"), ("hod", "malkuth"), ("yesod", "malkuth")]
+ADJ = {s: [] for s in SEPH}
+for i, (a, b) in enumerate(EDGES):
+ ADJ[a].append((i, b))
+ ADJ[b].append((i, a))
+
+
+def osub_seed(text, dim=D):
+ h = int(hashlib.sha256(text.lower().strip().encode()).hexdigest(), 16)
+ rng = np.random.RandomState(h % (2 ** 31))
+ return np.exp(1j * rng.uniform(0, 2 * np.pi, dim)).astype(np.complex64)
+
+
+def bind(a, b):
+ return np.fft.ifft(np.fft.fft(a) * np.fft.fft(b)).astype(np.complex64)
+
+
+def unbind(c, b):
+ return np.fft.ifft(np.fft.fft(c) * np.conj(np.fft.fft(b))).astype(np.complex64)
+
+
+K = np.stack([osub_seed("o7lam:%d" % e) for e in range(len(EDGES))])
+R = [osub_seed("o7pos:%d" % p) for p in range(64)]
+
+
+def path_vec(word):
+ return np.sum([bind(K[e], R[p]) for p, e in enumerate(word)], axis=0).astype(np.complex64)
+
+
+def decode_plain(vec, n):
+ out = []
+ for p in range(n):
+ sims = np.real(K.conj() @ unbind(vec, R[p]))
+ out.append(int(np.argmax(sims)))
+ return out
+
+
+def decode_tree(vec, n, start="malkuth"):
+ out, cur = [], start
+ for p in range(n):
+ est = unbind(vec, R[p])
+ cands = ADJ[cur]
+ sims = [float(np.real(np.vdot(K[e], est))) for e, _ in cands]
+ e, nxt = cands[int(np.argmax(sims))]
+ out.append(e)
+ cur = nxt
+ return out
+
+
+def walk(rng, n, start="malkuth"):
+ out, cur = [], start
+ for _ in range(n):
+ e, nxt = rng.choice(ADJ[cur])
+ out.append(e)
+ cur = nxt
+ return out
+
+
+def capacity(seed=4000, lengths=(6, 9, 12), bundles=(1, 2, 4, 8, 16, 32, 64), trials=60):
+ """exact-word recall of N walks bundled into ONE vector (each bound to its own key), plain cleanup vs the stabilizer"""
+ rng = random.Random(seed)
+ rows = []
+ for n in lengths:
+ for N in bundles:
+ ok_p = ok_t = tot = 0
+ for t in range(max(1, trials // N)):
+ words = [walk(rng, n) for _ in range(N)]
+ keys = [osub_seed("o7item:%d:%d:%d:%d" % (seed, n, t, i)) for i in range(N)]
+ M = np.sum([bind(k, path_vec(w)) for k, w in zip(keys, words)], axis=0)
+ for k, w in zip(keys, words):
+ v = unbind(M, k)
+ ok_p += decode_plain(v, n) == w
+ ok_t += decode_tree(v, n) == w
+ tot += 1
+ rows.append({"letters": n, "bundled": N, "words": tot, "plain": round(ok_p / tot, 4), "stabilized": round(ok_t / tot, 4)})
+ print(json.dumps(rows[-1]), flush=True)
+ return rows
+
+
+def cos(a, b):
+ return float(np.real(np.vdot(a, b)) / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-12))
+
+
+def reentry(entries, seed=4001):
+ """share of forms (answered words) re-entered by a later turn at cos >= TAU, and the same for shuffled words"""
+ words = [e["word"] for e in entries if e.get("status") == "answered" and e.get("word")]
+ rng = random.Random(seed)
+ shuf = [rng.sample(w, len(w)) for w in words]
+
+ def rate(ws):
+ vs = [path_vec(w) for w in ws]
+ hit = sum(1 for i in range(len(vs)) if any(cos(vs[i], vs[j]) >= TAU for j in range(i + 1, len(vs))))
+ return hit, len(vs)
+ h, n = rate(words)
+ hs, ns = rate(shuf)
+ return {"forms": n, "reentered": h, "rate": round(h / n, 4) if n else 0.0, "shuffled_reentered": hs,
+ "shuffled_rate": round(hs / ns, 4) if ns else 0.0, "tau": TAU}
+
+
+if __name__ == "__main__":
+ if sys.argv[1] == "capacity":
+ rows = capacity()
+ if len(sys.argv) > 2:
+ json.dump({"D": D, "codebook": "osub_seed o7lam/o7pos", "rows": rows}, open(sys.argv[2], "w"), indent=1)
+ elif sys.argv[1] == "reentry":
+ print(json.dumps(reentry([json.loads(l) for l in open(sys.argv[2])])))
diff --git a/root/specialists/_gate/standing_new_v1.txt b/root/specialists/_gate/standing_new_v1.txt
index e16167e8bdee22b9585d3c8f1c2fa07fe1ec1020..3d9e382f9e43b3a330f14fc25a9b42e2c2c8d353 100644
--- a/root/specialists/_gate/standing_new_v1.txt
+++ b/root/specialists/_gate/standing_new_v1.txt
@@ -1 +1 @@
-densities-r2 fluids-r2 decay springs fuels
+densities-r2 fluids-r2 decay-r2 springs fuels
diff --git a/root/specialists/_gate/standing_owner_v1.txt b/root/specialists/_gate/standing_owner_v1.txt
new file mode 100644
index 0000000000000000000000000000000000000000..2ca796fc049696e5c0f6017275819f342e3db8a2
--- /dev/null
+++ b/root/specialists/_gate/standing_owner_v1.txt
@@ -0,0 +1 @@
+orbits
diff --git a/root/specialists/_gate/standing_stars_v1.txt b/root/specialists/_gate/standing_stars_v1.txt
new file mode 100644
index 0000000000000000000000000000000000000000..81e205209933f01b41bdcbee769a81c82da8bb97
--- /dev/null
+++ b/root/specialists/_gate/standing_stars_v1.txt
@@ -0,0 +1,4 @@
+stars
+habzone
+exoorbits
+transits
diff --git a/root/specialists/_gate/star_panels_v1.sha256 b/root/specialists/_gate/star_panels_v1.sha256
new file mode 100644
index 0000000000000000000000000000000000000000..7bf8382f6edea3865e60bc50e89b61518b70ae04
--- /dev/null
+++ b/root/specialists/_gate/star_panels_v1.sha256
@@ -0,0 +1,4 @@
+ebf1be080508b21441419ea1f84794a0e5c280ab6a5ba12b767f895e43a091ba stars/stars_panel_v1.json
+0010261cb74a209cde736d4d21a257486ffe95e048f1ac54144be864846f0559 habzone/habzone_panel_v1.json
+e6ae236b37c1a456bb590113fdfefccb9185ff4e557f127bf5e7b929fa89bf74 exoorbits/exoorbits_panel_v1.json
+0ede1b605bbcf0c41024d57f0a1b4be5713d729678bd50c9990713ad66dc6135 transits/transits_panel_v1.json
diff --git a/root/specialists/_proved/NOTICE b/root/specialists/_proved/NOTICE
new file mode 100644
index 0000000000000000000000000000000000000000..88e318c9b38797e42fb0ee7ecb0f3621053db264
--- /dev/null
+++ b/root/specialists/_proved/NOTICE
@@ -0,0 +1,3 @@
+veri7vn proved kernels - https://github.com/emberian/veri7vn (fork https://github.com/psiloceyeben/veri7vn), by ember arlynx, CC0-1.0.
+Built from commit 3bf43cff8eaa1c30db2659d186cdcffe9c4f6c61: Isabelle2026-RC1 sessions (proofs checked during the build), generated Standard ML linked statically with the MLton in Isabelle, Linux x86-64. chemistry and anatomyagri link the regex.sml their own families carried before it was removed upstream (restored from git history).
+Used by Oracle7 fold w551 as a cross-check; see PROVED.json.
diff --git a/root/specialists/_proved/PROVED.json b/root/specialists/_proved/PROVED.json
new file mode 100644
index 0000000000000000000000000000000000000000..485fcdcf545e3ef07612e302fa52b045ccfc5e55
--- /dev/null
+++ b/root/specialists/_proved/PROVED.json
@@ -0,0 +1,806 @@
+{
+ "what": "veri7vn proved kernels standing in for Oracle7 specialists, used by fold w551 as a cross-check (a reply is marked proved when its kernel agrees, withheld when its kernel answers differently, unchanged otherwise).",
+ "source": "https://github.com/emberian/veri7vn (fork https://github.com/psiloceyeben/veri7vn)",
+ "commit": "3bf43cff8eaa1c30db2659d186cdcffe9c4f6c61",
+ "licence": "CC0-1.0",
+ "author": "ember arlynx",
+ "build": "Isabelle2026-RC1 sessions built (proofs checked) on the research box, generated Standard ML exported and linked statically with the MLton shipped in Isabelle (Linux x86-64).",
+ "parity": {
+ "heldout_questions": 7172,
+ "agree": 6036,
+ "organ_only": 1096,
+ "disagree": 40,
+ "note": "same held-out items; agree = same answer value (or both withheld); the disagreements measured were display rounding and ASCII vs Unicode working, not different answers"
+ },
+ "binaries": {
+ "acoustics": "4f22f4159849938674cbb4da325c7cde322f026b70306ea2f242bae05fd137b9",
+ "anatomyagri": "99df1f11b3920fc81f9462ffcf7e207f46da5309a9f96c3de3d9d371a8ba0fdd",
+ "chemistry": "e1292ae5a7fbc67ceeb94c38a0c9172379214a2ff7005ee1c9685e6ba460343c",
+ "climate": "15de19eb7a04447e23a7aa155ff05978c40f4ffb6d85163ddf7f9e143bcd5a92",
+ "computing": "52ff108928f06ee64ef41c34599027f0e98f1ac28964a4d227fa0d2b191e3f5c",
+ "discrete": "555718394b624bec0a90615ba3feef312c5fd0bb037ca05b155602a4e28e0a37",
+ "emoptics": "f013d847f239bed7f616fc9a007b3831c99cfb73a7a5030be1317d4d9cdfe071",
+ "energy": "df8449c8284dda0b63a0f6d40767e4721e3cdb59682344f6f30e0ea4c5580ea8",
+ "facts": "746bab8b08c15024bae763c9627b9874d3b21ba4623b0107d604bc6bd8fe0719",
+ "fusion": "0f765ab3ca5b4b6a405e6401ac8b3f6ea915d38992fbf1d176ed7837019ee969",
+ "geostats": "f92a48c4c941846bda06de9bec32bb9238f60edde022fd844334b7d73905680b",
+ "humanities": "3befad7a8d17c9bee411fd38e658866e93e8f6d2c325f641a08ce2acc5facd5e",
+ "life": "65748246c35730eeed5e733017ac0590193d4dfad5423125eb393461fc3fb0e2",
+ "materials": "6a548f83db9973f647fa43a16860c56bc933f6a40a2a53dfafb056d24ecdffe4",
+ "mechanics": "7c636d4ca6a1b8f66cfe11fc3b51dcc221efb252565828fe69056257c6aa1a25",
+ "quantity": "368849c4e9cee834ee609f4b6617f7afac2c85817a2bf664960f2e0cafdbc976",
+ "space": "725a57a7ec25a13f1a8f155183013520b3c1636c91cd3a90c4440e06a0c0552e",
+ "stores": "20ebbd038131be04642d80626dfa1fca6c03430f6e0bc7f6a8ca726633628e68",
+ "trades": "cb3f5063d873f68daeec59cc031d1492ef0cab7ac4a2fa9b5ab3db78fd3eaecf"
+ },
+ "specialists": {
+ "acoustics": {
+ "binary": "acoustics",
+ "flag": false,
+ "family": "acoustics",
+ "session": "Veri7vn_Acoustics"
+ },
+ "anatomy": {
+ "binary": "anatomyagri",
+ "flag": true,
+ "family": "anatomyagri",
+ "session": "Veri7vn_AnatomyAgri"
+ },
+ "anatomy-r2": {
+ "binary": "anatomyagri",
+ "flag": true,
+ "family": "anatomyagri",
+ "session": "Veri7vn_AnatomyAgri"
+ },
+ "agriculture-v2": {
+ "binary": "anatomyagri",
+ "flag": true,
+ "family": "anatomyagri",
+ "session": "Veri7vn_AnatomyAgri"
+ },
+ "gardening": {
+ "binary": "anatomyagri",
+ "flag": true,
+ "family": "anatomyagri",
+ "session": "Veri7vn_AnatomyAgri"
+ },
+ "nutrition-label-v2": {
+ "binary": "anatomyagri",
+ "flag": true,
+ "family": "anatomyagri",
+ "session": "Veri7vn_AnatomyAgri"
+ },
+ "chemistry": {
+ "binary": "chemistry",
+ "flag": true,
+ "family": "chemistry",
+ "session": "Veri7vn_Chemistry"
+ },
+ "chemistry-v3": {
+ "binary": "chemistry",
+ "flag": true,
+ "family": "chemistry",
+ "session": "Veri7vn_Chemistry"
+ },
+ "decay": {
+ "binary": "chemistry",
+ "flag": true,
+ "family": "chemistry",
+ "session": "Veri7vn_Chemistry"
+ },
+ "labsolutions": {
+ "binary": "chemistry",
+ "flag": true,
+ "family": "chemistry",
+ "session": "Veri7vn_Chemistry"
+ },
+ "labsolutions-r2": {
+ "binary": "chemistry",
+ "flag": true,
+ "family": "chemistry",
+ "session": "Veri7vn_Chemistry"
+ },
+ "carboncapture": {
+ "binary": "climate",
+ "flag": true,
+ "family": "climate",
+ "session": "Veri7vn_Climate"
+ },
+ "airquality": {
+ "binary": "climate",
+ "flag": true,
+ "family": "climate",
+ "session": "Veri7vn_Climate"
+ },
+ "climate": {
+ "binary": "climate",
+ "flag": true,
+ "family": "climate",
+ "session": "Veri7vn_Climate"
+ },
+ "carbonmat": {
+ "binary": "climate",
+ "flag": true,
+ "family": "climate",
+ "session": "Veri7vn_Climate"
+ },
+ "coldchain": {
+ "binary": "climate",
+ "flag": true,
+ "family": "climate",
+ "session": "Veri7vn_Climate"
+ },
+ "coldchain-v2": {
+ "binary": "climate",
+ "flag": true,
+ "family": "climate",
+ "session": "Veri7vn_Climate"
+ },
+ "networking": {
+ "binary": "computing",
+ "flag": true,
+ "family": "computing",
+ "session": "Veri7vn_Computing"
+ },
+ "embedded": {
+ "binary": "computing",
+ "flag": true,
+ "family": "computing",
+ "session": "Veri7vn_Computing"
+ },
+ "cyber": {
+ "binary": "computing",
+ "flag": true,
+ "family": "computing",
+ "session": "Veri7vn_Computing"
+ },
+ "linux-v2": {
+ "binary": "computing",
+ "flag": true,
+ "family": "computing",
+ "session": "Veri7vn_Computing"
+ },
+ "git": {
+ "binary": "computing",
+ "flag": true,
+ "family": "computing",
+ "session": "Veri7vn_Computing"
+ },
+ "sql": {
+ "binary": "computing",
+ "flag": true,
+ "family": "computing",
+ "session": "Veri7vn_Computing"
+ },
+ "python": {
+ "binary": "computing",
+ "flag": true,
+ "family": "computing",
+ "session": "Veri7vn_Computing"
+ },
+ "python-r2": {
+ "binary": "computing",
+ "flag": true,
+ "family": "computing",
+ "session": "Veri7vn_Computing"
+ },
+ "robotics-v2": {
+ "binary": "computing",
+ "flag": true,
+ "family": "computing",
+ "session": "Veri7vn_Computing"
+ },
+ "games-v3": {
+ "binary": "discrete",
+ "flag": true,
+ "family": "discrete",
+ "session": "Veri7vn_Discrete"
+ },
+ "logic-v3": {
+ "binary": "discrete",
+ "flag": true,
+ "family": "discrete",
+ "session": "Veri7vn_Discrete"
+ },
+ "dataformats": {
+ "binary": "discrete",
+ "flag": true,
+ "family": "discrete",
+ "session": "Veri7vn_Discrete"
+ },
+ "algorithms": {
+ "binary": "discrete",
+ "flag": true,
+ "family": "discrete",
+ "session": "Veri7vn_Discrete"
+ },
+ "calculus": {
+ "binary": "discrete",
+ "flag": true,
+ "family": "discrete",
+ "session": "Veri7vn_Discrete"
+ },
+ "electrical": {
+ "binary": "emoptics",
+ "flag": true,
+ "family": "emoptics",
+ "session": "Veri7vn_Emoptics"
+ },
+ "electronics": {
+ "binary": "emoptics",
+ "flag": true,
+ "family": "emoptics",
+ "session": "Veri7vn_Emoptics"
+ },
+ "electronics-r2": {
+ "binary": "emoptics",
+ "flag": true,
+ "family": "emoptics",
+ "session": "Veri7vn_Emoptics"
+ },
+ "wiring": {
+ "binary": "emoptics",
+ "flag": true,
+ "family": "emoptics",
+ "session": "Veri7vn_Emoptics"
+ },
+ "optics": {
+ "binary": "emoptics",
+ "flag": true,
+ "family": "emoptics",
+ "session": "Veri7vn_Emoptics"
+ },
+ "optics-v2": {
+ "binary": "emoptics",
+ "flag": true,
+ "family": "emoptics",
+ "session": "Veri7vn_Emoptics"
+ },
+ "optics-v3": {
+ "binary": "emoptics",
+ "flag": true,
+ "family": "emoptics",
+ "session": "Veri7vn_Emoptics"
+ },
+ "optics-v5": {
+ "binary": "emoptics",
+ "flag": true,
+ "family": "emoptics",
+ "session": "Veri7vn_Emoptics"
+ },
+ "optics-v5-r2": {
+ "binary": "emoptics",
+ "flag": true,
+ "family": "emoptics",
+ "session": "Veri7vn_Emoptics"
+ },
+ "thermo": {
+ "binary": "energy",
+ "flag": true,
+ "family": "energy",
+ "session": "Veri7vn_Energy"
+ },
+ "energy-v2": {
+ "binary": "energy",
+ "flag": true,
+ "family": "energy",
+ "session": "Veri7vn_Energy"
+ },
+ "energy-v3": {
+ "binary": "energy",
+ "flag": true,
+ "family": "energy",
+ "session": "Veri7vn_Energy"
+ },
+ "hvac": {
+ "binary": "energy",
+ "flag": true,
+ "family": "energy",
+ "session": "Veri7vn_Energy"
+ },
+ "renewables": {
+ "binary": "energy",
+ "flag": true,
+ "family": "energy",
+ "session": "Veri7vn_Energy"
+ },
+ "iot": {
+ "binary": "energy",
+ "flag": true,
+ "family": "energy",
+ "session": "Veri7vn_Energy"
+ },
+ "literature-v2": {
+ "binary": "facts",
+ "flag": true,
+ "family": "facts",
+ "session": "Veri7vn_Facts"
+ },
+ "worldhistory": {
+ "binary": "facts",
+ "flag": true,
+ "family": "facts",
+ "session": "Veri7vn_Facts"
+ },
+ "arthistory": {
+ "binary": "facts",
+ "flag": true,
+ "family": "facts",
+ "session": "Veri7vn_Facts"
+ },
+ "medicine-r2": {
+ "binary": "facts",
+ "flag": true,
+ "family": "facts",
+ "session": "Veri7vn_Facts"
+ },
+ "medicine": {
+ "binary": "facts",
+ "flag": true,
+ "family": "facts",
+ "session": "Veri7vn_Facts"
+ },
+ "law": {
+ "binary": "facts",
+ "flag": true,
+ "family": "facts",
+ "session": "Veri7vn_Facts"
+ },
+ "psychology": {
+ "binary": "facts",
+ "flag": true,
+ "family": "facts",
+ "session": "Veri7vn_Facts"
+ },
+ "sociology": {
+ "binary": "facts",
+ "flag": true,
+ "family": "facts",
+ "session": "Veri7vn_Facts"
+ },
+ "linguistics": {
+ "binary": "facts",
+ "flag": true,
+ "family": "facts",
+ "session": "Veri7vn_Facts"
+ },
+ "poetry": {
+ "binary": "facts",
+ "flag": true,
+ "family": "facts",
+ "session": "Veri7vn_Facts"
+ },
+ "botany": {
+ "binary": "facts",
+ "flag": true,
+ "family": "facts",
+ "session": "Veri7vn_Facts"
+ },
+ "entomology": {
+ "binary": "facts",
+ "flag": true,
+ "family": "facts",
+ "session": "Veri7vn_Facts"
+ },
+ "marinebio": {
+ "binary": "facts",
+ "flag": true,
+ "family": "facts",
+ "session": "Veri7vn_Facts"
+ },
+ "ornithology": {
+ "binary": "facts",
+ "flag": true,
+ "family": "facts",
+ "session": "Veri7vn_Facts"
+ },
+ "mycology": {
+ "binary": "facts",
+ "flag": true,
+ "family": "facts",
+ "session": "Veri7vn_Facts"
+ },
+ "mycelial": {
+ "binary": "facts",
+ "flag": true,
+ "family": "facts",
+ "session": "Veri7vn_Facts"
+ },
+ "pets": {
+ "binary": "facts",
+ "flag": true,
+ "family": "facts",
+ "session": "Veri7vn_Facts"
+ },
+ "childdev": {
+ "binary": "facts",
+ "flag": true,
+ "family": "facts",
+ "session": "Veri7vn_Facts"
+ },
+ "etiquette": {
+ "binary": "facts",
+ "flag": true,
+ "family": "facts",
+ "session": "Veri7vn_Facts"
+ },
+ "firesafety-v2": {
+ "binary": "facts",
+ "flag": true,
+ "family": "facts",
+ "session": "Veri7vn_Facts"
+ },
+ "firstaid": {
+ "binary": "facts",
+ "flag": true,
+ "family": "facts",
+ "session": "Veri7vn_Facts"
+ },
+ "javascript": {
+ "binary": "facts",
+ "flag": true,
+ "family": "facts",
+ "session": "Veri7vn_Facts"
+ },
+ "geometry": {
+ "binary": "geostats",
+ "flag": true,
+ "family": "geostats",
+ "session": "Veri7vn_GeoStats"
+ },
+ "geometry-v3": {
+ "binary": "geostats",
+ "flag": true,
+ "family": "geostats",
+ "session": "Veri7vn_GeoStats"
+ },
+ "geometry-v4": {
+ "binary": "geostats",
+ "flag": true,
+ "family": "geostats",
+ "session": "Veri7vn_GeoStats"
+ },
+ "statistics": {
+ "binary": "geostats",
+ "flag": true,
+ "family": "geostats",
+ "session": "Veri7vn_GeoStats"
+ },
+ "statistics-v2": {
+ "binary": "geostats",
+ "flag": true,
+ "family": "geostats",
+ "session": "Veri7vn_GeoStats"
+ },
+ "statistics-v3": {
+ "binary": "geostats",
+ "flag": true,
+ "family": "geostats",
+ "session": "Veri7vn_GeoStats"
+ },
+ "music-v2-r2": {
+ "binary": "humanities",
+ "flag": true,
+ "family": "humanities",
+ "session": "Veri7vn_Humanities"
+ },
+ "music-v2": {
+ "binary": "humanities",
+ "flag": true,
+ "family": "humanities",
+ "session": "Veri7vn_Humanities"
+ },
+ "grammar": {
+ "binary": "humanities",
+ "flag": true,
+ "family": "humanities",
+ "session": "Veri7vn_Humanities"
+ },
+ "mythology": {
+ "binary": "humanities",
+ "flag": true,
+ "family": "humanities",
+ "session": "Veri7vn_Humanities"
+ },
+ "ushistory": {
+ "binary": "humanities",
+ "flag": true,
+ "family": "humanities",
+ "session": "Veri7vn_Humanities"
+ },
+ "library": {
+ "binary": "humanities",
+ "flag": true,
+ "family": "humanities",
+ "session": "Veri7vn_Humanities"
+ },
+ "government": {
+ "binary": "humanities",
+ "flag": true,
+ "family": "humanities",
+ "session": "Veri7vn_Humanities"
+ },
+ "genetics-v2": {
+ "binary": "life",
+ "flag": true,
+ "family": "life",
+ "session": "Veri7vn_Life"
+ },
+ "biology": {
+ "binary": "life",
+ "flag": true,
+ "family": "life",
+ "session": "Veri7vn_Life"
+ },
+ "ecology": {
+ "binary": "life",
+ "flag": true,
+ "family": "life",
+ "session": "Veri7vn_Life"
+ },
+ "forestry": {
+ "binary": "life",
+ "flag": true,
+ "family": "life",
+ "session": "Veri7vn_Life"
+ },
+ "wildlife": {
+ "binary": "life",
+ "flag": true,
+ "family": "life",
+ "session": "Veri7vn_Life"
+ },
+ "microbiology-v2": {
+ "binary": "life",
+ "flag": true,
+ "family": "life",
+ "session": "Veri7vn_Life"
+ },
+ "additive-v2": {
+ "binary": "materials",
+ "flag": true,
+ "family": "materials",
+ "session": "Veri7vn_Materials"
+ },
+ "materials-v2": {
+ "binary": "materials",
+ "flag": true,
+ "family": "materials",
+ "session": "Veri7vn_Materials"
+ },
+ "soil": {
+ "binary": "materials",
+ "flag": true,
+ "family": "materials",
+ "session": "Veri7vn_Materials"
+ },
+ "soil-r2": {
+ "binary": "materials",
+ "flag": true,
+ "family": "materials",
+ "session": "Veri7vn_Materials"
+ },
+ "water": {
+ "binary": "materials",
+ "flag": true,
+ "family": "materials",
+ "session": "Veri7vn_Materials"
+ },
+ "water-r3": {
+ "binary": "materials",
+ "flag": true,
+ "family": "materials",
+ "session": "Veri7vn_Materials"
+ },
+ "physics": {
+ "binary": "mechanics",
+ "flag": true,
+ "family": "mechanics",
+ "session": "Veri7vn_Mechanics"
+ },
+ "physics-r2": {
+ "binary": "mechanics",
+ "flag": true,
+ "family": "mechanics",
+ "session": "Veri7vn_Mechanics"
+ },
+ "physics-r3": {
+ "binary": "mechanics",
+ "flag": true,
+ "family": "mechanics",
+ "session": "Veri7vn_Mechanics"
+ },
+ "mechanisms": {
+ "binary": "mechanics",
+ "flag": true,
+ "family": "mechanics",
+ "session": "Veri7vn_Mechanics"
+ },
+ "mechanisms-v2": {
+ "binary": "mechanics",
+ "flag": true,
+ "family": "mechanics",
+ "session": "Veri7vn_Mechanics"
+ },
+ "bodies-v2": {
+ "binary": "mechanics",
+ "flag": true,
+ "family": "mechanics",
+ "session": "Veri7vn_Mechanics"
+ },
+ "sports": {
+ "binary": "mechanics",
+ "flag": true,
+ "family": "mechanics",
+ "session": "Veri7vn_Mechanics"
+ },
+ "fleet": {
+ "binary": "mechanics",
+ "flag": true,
+ "family": "mechanics",
+ "session": "Veri7vn_Mechanics"
+ },
+ "fluids-r2": {
+ "binary": "mechanics",
+ "flag": true,
+ "family": "mechanics",
+ "session": "Veri7vn_Mechanics"
+ },
+ "densities": {
+ "binary": "quantity",
+ "flag": true,
+ "family": "quantity",
+ "session": "Veri7vn_Quantity"
+ },
+ "units": {
+ "binary": "quantity",
+ "flag": true,
+ "family": "quantity",
+ "session": "Veri7vn_Quantity"
+ },
+ "units-v3": {
+ "binary": "quantity",
+ "flag": true,
+ "family": "quantity",
+ "session": "Veri7vn_Quantity"
+ },
+ "springs": {
+ "binary": "quantity",
+ "flag": true,
+ "family": "quantity",
+ "session": "Veri7vn_Quantity"
+ },
+ "fluids": {
+ "binary": "quantity",
+ "flag": true,
+ "family": "quantity",
+ "session": "Veri7vn_Quantity"
+ },
+ "fuels": {
+ "binary": "quantity",
+ "flag": true,
+ "family": "quantity",
+ "session": "Veri7vn_Quantity"
+ },
+ "orbits": {
+ "binary": "space",
+ "flag": true,
+ "family": "space",
+ "session": "Veri7vn_Space"
+ },
+ "planetary": {
+ "binary": "space",
+ "flag": true,
+ "family": "space",
+ "session": "Veri7vn_Space"
+ },
+ "spacecraft": {
+ "binary": "space",
+ "flag": true,
+ "family": "space",
+ "session": "Veri7vn_Space"
+ },
+ "spacecomms": {
+ "binary": "space",
+ "flag": true,
+ "family": "space",
+ "session": "Veri7vn_Space"
+ },
+ "astronomy": {
+ "binary": "space",
+ "flag": true,
+ "family": "space",
+ "session": "Veri7vn_Space"
+ },
+ "astronomy-v2": {
+ "binary": "space",
+ "flag": true,
+ "family": "space",
+ "session": "Veri7vn_Space"
+ },
+ "astronomy-v3": {
+ "binary": "space",
+ "flag": true,
+ "family": "space",
+ "session": "Veri7vn_Space"
+ },
+ "astronomy-v3-r2": {
+ "binary": "space",
+ "flag": true,
+ "family": "space",
+ "session": "Veri7vn_Space"
+ },
+ "cooking": {
+ "binary": "stores",
+ "flag": true,
+ "family": "stores",
+ "session": "Veri7vn_Stores"
+ },
+ "cooking-v2": {
+ "binary": "stores",
+ "flag": true,
+ "family": "stores",
+ "session": "Veri7vn_Stores"
+ },
+ "cooking-v3": {
+ "binary": "stores",
+ "flag": true,
+ "family": "stores",
+ "session": "Veri7vn_Stores"
+ },
+ "cooking-v3-r2": {
+ "binary": "stores",
+ "flag": true,
+ "family": "stores",
+ "session": "Veri7vn_Stores"
+ },
+ "warehouse": {
+ "binary": "stores",
+ "flag": true,
+ "family": "stores",
+ "session": "Veri7vn_Stores"
+ },
+ "warehouse-r2": {
+ "binary": "stores",
+ "flag": true,
+ "family": "stores",
+ "session": "Veri7vn_Stores"
+ },
+ "architecture": {
+ "binary": "trades",
+ "flag": true,
+ "family": "trades",
+ "session": "Veri7vn_Trades"
+ },
+ "printing": {
+ "binary": "trades",
+ "flag": true,
+ "family": "trades",
+ "session": "Veri7vn_Trades"
+ },
+ "printing-v2": {
+ "binary": "trades",
+ "flag": true,
+ "family": "trades",
+ "session": "Veri7vn_Trades"
+ },
+ "safety": {
+ "binary": "trades",
+ "flag": true,
+ "family": "trades",
+ "session": "Veri7vn_Trades"
+ },
+ "woodworking": {
+ "binary": "trades",
+ "flag": true,
+ "family": "trades",
+ "session": "Veri7vn_Trades"
+ },
+ "crafts": {
+ "binary": "trades",
+ "flag": true,
+ "family": "trades",
+ "session": "Veri7vn_Trades"
+ }
+ }
+}
\ No newline at end of file
diff --git a/root/specialists/_proved/SHA256SUMS b/root/specialists/_proved/SHA256SUMS
new file mode 100644
index 0000000000000000000000000000000000000000..c027318f1904791ca93baf35052a2cfd29327f20
--- /dev/null
+++ b/root/specialists/_proved/SHA256SUMS
@@ -0,0 +1,20 @@
+4f22f4159849938674cbb4da325c7cde322f026b70306ea2f242bae05fd137b9 acoustics
+99df1f11b3920fc81f9462ffcf7e207f46da5309a9f96c3de3d9d371a8ba0fdd anatomyagri
+e1292ae5a7fbc67ceeb94c38a0c9172379214a2ff7005ee1c9685e6ba460343c chemistry
+15de19eb7a04447e23a7aa155ff05978c40f4ffb6d85163ddf7f9e143bcd5a92 climate
+52ff108928f06ee64ef41c34599027f0e98f1ac28964a4d227fa0d2b191e3f5c computing
+f948026d0935b07a3e073be0d94c5c0e9d567973cbb88c10fb915001cd14e563 decide
+555718394b624bec0a90615ba3feef312c5fd0bb037ca05b155602a4e28e0a37 discrete
+f013d847f239bed7f616fc9a007b3831c99cfb73a7a5030be1317d4d9cdfe071 emoptics
+df8449c8284dda0b63a0f6d40767e4721e3cdb59682344f6f30e0ea4c5580ea8 energy
+746bab8b08c15024bae763c9627b9874d3b21ba4623b0107d604bc6bd8fe0719 facts
+0f765ab3ca5b4b6a405e6401ac8b3f6ea915d38992fbf1d176ed7837019ee969 fusion
+f92a48c4c941846bda06de9bec32bb9238f60edde022fd844334b7d73905680b geostats
+3befad7a8d17c9bee411fd38e658866e93e8f6d2c325f641a08ce2acc5facd5e humanities
+65748246c35730eeed5e733017ac0590193d4dfad5423125eb393461fc3fb0e2 life
+6a548f83db9973f647fa43a16860c56bc933f6a40a2a53dfafb056d24ecdffe4 materials
+7c636d4ca6a1b8f66cfe11fc3b51dcc221efb252565828fe69056257c6aa1a25 mechanics
+368849c4e9cee834ee609f4b6617f7afac2c85817a2bf664960f2e0cafdbc976 quantity
+725a57a7ec25a13f1a8f155183013520b3c1636c91cd3a90c4440e06a0c0552e space
+20ebbd038131be04642d80626dfa1fca6c03430f6e0bc7f6a8ca726633628e68 stores
+cb3f5063d873f68daeec59cc031d1492ef0cab7ac4a2fa9b5ab3db78fd3eaecf trades
diff --git a/root/specialists/_proved/bin/acoustics b/root/specialists/_proved/bin/acoustics
new file mode 100644
index 0000000000000000000000000000000000000000..bb6baf0f945043bb22337a99f722e1f097e2ff31
--- /dev/null
+++ b/root/specialists/_proved/bin/acoustics
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4f22f4159849938674cbb4da325c7cde322f026b70306ea2f242bae05fd137b9
+size 1939384
diff --git a/root/specialists/_proved/bin/anatomyagri b/root/specialists/_proved/bin/anatomyagri
new file mode 100644
index 0000000000000000000000000000000000000000..d9cee091c930d30d13eba155b8692b509031a8fe
--- /dev/null
+++ b/root/specialists/_proved/bin/anatomyagri
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:99df1f11b3920fc81f9462ffcf7e207f46da5309a9f96c3de3d9d371a8ba0fdd
+size 1920688
diff --git a/root/specialists/_proved/bin/chemistry b/root/specialists/_proved/bin/chemistry
new file mode 100644
index 0000000000000000000000000000000000000000..97b5387340a59d1de12a2f51a2a185f53f7a9168
--- /dev/null
+++ b/root/specialists/_proved/bin/chemistry
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e1292ae5a7fbc67ceeb94c38a0c9172379214a2ff7005ee1c9685e6ba460343c
+size 2518624
diff --git a/root/specialists/_proved/bin/climate b/root/specialists/_proved/bin/climate
new file mode 100644
index 0000000000000000000000000000000000000000..5ee75aab7c9517e199b47727c285cfbeb1aaca2a
--- /dev/null
+++ b/root/specialists/_proved/bin/climate
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:15de19eb7a04447e23a7aa155ff05978c40f4ffb6d85163ddf7f9e143bcd5a92
+size 2281096
diff --git a/root/specialists/_proved/bin/computing b/root/specialists/_proved/bin/computing
new file mode 100644
index 0000000000000000000000000000000000000000..d370fbab28c1a7dfb7733ec4f77c3d0c9aef59bc
--- /dev/null
+++ b/root/specialists/_proved/bin/computing
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:52ff108928f06ee64ef41c34599027f0e98f1ac28964a4d227fa0d2b191e3f5c
+size 3430376
diff --git a/root/specialists/_proved/bin/decide b/root/specialists/_proved/bin/decide
new file mode 100644
index 0000000000000000000000000000000000000000..5353234a370e179d985cd5abbf7a6451775a518d
--- /dev/null
+++ b/root/specialists/_proved/bin/decide
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f948026d0935b07a3e073be0d94c5c0e9d567973cbb88c10fb915001cd14e563
+size 2098832
diff --git a/root/specialists/_proved/bin/discrete b/root/specialists/_proved/bin/discrete
new file mode 100644
index 0000000000000000000000000000000000000000..af3ee5d1c962652434bbf64dcd7b25b668126d1e
--- /dev/null
+++ b/root/specialists/_proved/bin/discrete
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:555718394b624bec0a90615ba3feef312c5fd0bb037ca05b155602a4e28e0a37
+size 2931232
diff --git a/root/specialists/_proved/bin/emoptics b/root/specialists/_proved/bin/emoptics
new file mode 100644
index 0000000000000000000000000000000000000000..e0327ec694d0c41151ad48385839fc30e575fa6a
--- /dev/null
+++ b/root/specialists/_proved/bin/emoptics
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f013d847f239bed7f616fc9a007b3831c99cfb73a7a5030be1317d4d9cdfe071
+size 2665960
diff --git a/root/specialists/_proved/bin/energy b/root/specialists/_proved/bin/energy
new file mode 100644
index 0000000000000000000000000000000000000000..c6e27f3df8089738f40a945ee2d8679507629b19
--- /dev/null
+++ b/root/specialists/_proved/bin/energy
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:df8449c8284dda0b63a0f6d40767e4721e3cdb59682344f6f30e0ea4c5580ea8
+size 2540072
diff --git a/root/specialists/_proved/bin/facts b/root/specialists/_proved/bin/facts
new file mode 100644
index 0000000000000000000000000000000000000000..c00d5a820f4534a136264792af8c4958585785b3
--- /dev/null
+++ b/root/specialists/_proved/bin/facts
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:746bab8b08c15024bae763c9627b9874d3b21ba4623b0107d604bc6bd8fe0719
+size 1638712
diff --git a/root/specialists/_proved/bin/fusion b/root/specialists/_proved/bin/fusion
new file mode 100644
index 0000000000000000000000000000000000000000..6ae6342439e03ec50f552d357686aca8ea1ffc9f
--- /dev/null
+++ b/root/specialists/_proved/bin/fusion
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0f765ab3ca5b4b6a405e6401ac8b3f6ea915d38992fbf1d176ed7837019ee969
+size 1914848
diff --git a/root/specialists/_proved/bin/geostats b/root/specialists/_proved/bin/geostats
new file mode 100644
index 0000000000000000000000000000000000000000..f690f35380293e8bea7a037e73197e4418829bbf
--- /dev/null
+++ b/root/specialists/_proved/bin/geostats
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f92a48c4c941846bda06de9bec32bb9238f60edde022fd844334b7d73905680b
+size 2269952
diff --git a/root/specialists/_proved/bin/humanities b/root/specialists/_proved/bin/humanities
new file mode 100644
index 0000000000000000000000000000000000000000..31300128c2e8261f30a153b3d49591a97966a9b9
--- /dev/null
+++ b/root/specialists/_proved/bin/humanities
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3befad7a8d17c9bee411fd38e658866e93e8f6d2c325f641a08ce2acc5facd5e
+size 2344592
diff --git a/root/specialists/_proved/bin/life b/root/specialists/_proved/bin/life
new file mode 100644
index 0000000000000000000000000000000000000000..05f44319725f4b10e515af14699e012c13b1c53f
--- /dev/null
+++ b/root/specialists/_proved/bin/life
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:65748246c35730eeed5e733017ac0590193d4dfad5423125eb393461fc3fb0e2
+size 2575840
diff --git a/root/specialists/_proved/bin/materials b/root/specialists/_proved/bin/materials
new file mode 100644
index 0000000000000000000000000000000000000000..a97512e179185aa21c12cd20909a5847c3fa747b
--- /dev/null
+++ b/root/specialists/_proved/bin/materials
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6a548f83db9973f647fa43a16860c56bc933f6a40a2a53dfafb056d24ecdffe4
+size 2692744
diff --git a/root/specialists/_proved/bin/mechanics b/root/specialists/_proved/bin/mechanics
new file mode 100644
index 0000000000000000000000000000000000000000..6a0f53d3ea5214eca5a28e23afb62ab7d7822473
--- /dev/null
+++ b/root/specialists/_proved/bin/mechanics
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7c636d4ca6a1b8f66cfe11fc3b51dcc221efb252565828fe69056257c6aa1a25
+size 2413192
diff --git a/root/specialists/_proved/bin/quantity b/root/specialists/_proved/bin/quantity
new file mode 100644
index 0000000000000000000000000000000000000000..3b4375fb3748de4963ec57f6c1567ef4daece8f5
--- /dev/null
+++ b/root/specialists/_proved/bin/quantity
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:368849c4e9cee834ee609f4b6617f7afac2c85817a2bf664960f2e0cafdbc976
+size 2229104
diff --git a/root/specialists/_proved/bin/space b/root/specialists/_proved/bin/space
new file mode 100644
index 0000000000000000000000000000000000000000..e8622f2592ee70ee3baa3c50da105bdf05f185fe
--- /dev/null
+++ b/root/specialists/_proved/bin/space
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:725a57a7ec25a13f1a8f155183013520b3c1636c91cd3a90c4440e06a0c0552e
+size 2530424
diff --git a/root/specialists/_proved/bin/stores b/root/specialists/_proved/bin/stores
new file mode 100644
index 0000000000000000000000000000000000000000..fbac2b3616a856f70c63770b0ca9e5528f58d389
--- /dev/null
+++ b/root/specialists/_proved/bin/stores
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:20ebbd038131be04642d80626dfa1fca6c03430f6e0bc7f6a8ca726633628e68
+size 2353968
diff --git a/root/specialists/_proved/bin/trades b/root/specialists/_proved/bin/trades
new file mode 100644
index 0000000000000000000000000000000000000000..ae90b9daf7f0da30f2a93942e9c6fa2cccaceade
--- /dev/null
+++ b/root/specialists/_proved/bin/trades
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cb3f5063d873f68daeec59cc031d1492ef0cab7ac4a2fa9b5ab3db78fd3eaecf
+size 2723040
diff --git a/root/specialists/decay-r2/INHERITED.json b/root/specialists/decay-r2/INHERITED.json
new file mode 100644
index 0000000000000000000000000000000000000000..b6b28a29cefd7b19da94df803aac6761ef574a4c
--- /dev/null
+++ b/root/specialists/decay-r2/INHERITED.json
@@ -0,0 +1,8 @@
+{
+ "id": "decay-r2",
+ "inherits": "decay",
+ "why": "narrowing patch r2 (sealed composition test C4, item c4-28): a remaining amount is converted to the mass unit the question asks for",
+ "invariance": "invariance_v1 over the decay panel: dev 0 of 42 and held-out 0 of 42 replies differ, so the held-out measurement carries over (factory rule 2026-09-29)",
+ "core_overlap": "0 of 3107 core items taken",
+ "time": "2026-10-18T04:40:01Z"
+}
\ No newline at end of file
diff --git a/root/specialists/decay-r2/decay_panel_v1.json b/root/specialists/decay-r2/decay_panel_v1.json
new file mode 100644
index 0000000000000000000000000000000000000000..0421b2e9401dc3bd1ed0bc4239e9c792c80edd7f
--- /dev/null
+++ b/root/specialists/decay-r2/decay_panel_v1.json
@@ -0,0 +1,590 @@
+[
+ {
+ "split": "dev",
+ "kind": "remaining_amount",
+ "valid": true,
+ "q": "A sample has 50 g of americium-241. How much remains after 432.6 years?",
+ "expect": 25.0
+ },
+ {
+ "split": "dev",
+ "kind": "remaining_amount",
+ "valid": true,
+ "q": "A substance has a half-life of 12 days. How much of 40 mg remains after 24 days?",
+ "expect": 10.0
+ },
+ {
+ "split": "dev",
+ "kind": "remaining_fraction",
+ "valid": true,
+ "q": "What fraction of technetium-99m remains after 12.01 hours?",
+ "expect": 0.2500981055652238
+ },
+ {
+ "split": "dev",
+ "kind": "time_to_fraction",
+ "valid": true,
+ "q": "How many years until only 50% of a sample of tritium remains?",
+ "expect": 12.32
+ },
+ {
+ "split": "dev",
+ "kind": "half_lives",
+ "valid": true,
+ "q": "How many half-lives of carbon-14 pass in 11460 years?",
+ "expect": 2.0
+ },
+ {
+ "split": "dev",
+ "kind": "remaining_amount",
+ "valid": true,
+ "q": "A sample has 100 g of caesium-137. How much remains after 15.04 years?",
+ "expect": 70.71067811865476
+ },
+ {
+ "split": "dev",
+ "kind": "remaining_amount",
+ "valid": true,
+ "q": "A substance has a half-life of 8 days. How much of 40 mg remains after 4 days?",
+ "expect": 28.284271247461902
+ },
+ {
+ "split": "dev",
+ "kind": "remaining_fraction",
+ "valid": true,
+ "q": "What fraction of radon-222 remains after 11.47 days?",
+ "expect": 0.12501133089113353
+ },
+ {
+ "split": "dev",
+ "kind": "time_to_fraction",
+ "valid": true,
+ "q": "How many days until only 10% of a sample of phosphorus-32 remains?",
+ "expect": 47.39727005785289
+ },
+ {
+ "split": "dev",
+ "kind": "half_lives",
+ "valid": true,
+ "q": "How many half-lives of radon-222 pass in 17.21 days?",
+ "expect": 4.5011115470119005
+ },
+ {
+ "split": "dev",
+ "kind": "remaining_amount",
+ "valid": true,
+ "q": "A sample has 250 g of phosphorus-32. How much remains after 7.13 days?",
+ "expect": 176.81105026553874
+ },
+ {
+ "split": "dev",
+ "kind": "remaining_amount",
+ "valid": true,
+ "q": "A substance has a half-life of 12 days. How much of 200 mg remains after 10 days?",
+ "expect": 112.2462048309373
+ },
+ {
+ "split": "dev",
+ "kind": "remaining_fraction",
+ "valid": true,
+ "q": "What fraction of carbon-14 remains after 5730 years?",
+ "expect": 0.5
+ },
+ {
+ "split": "dev",
+ "kind": "time_to_fraction",
+ "valid": true,
+ "q": "How many years until only 50% of a sample of caesium-137 remains?",
+ "expect": 30.08
+ },
+ {
+ "split": "dev",
+ "kind": "half_lives",
+ "valid": true,
+ "q": "How many half-lives of radon-222 pass in 7.65 days?",
+ "expect": 2.000784621420165
+ },
+ {
+ "split": "dev",
+ "kind": "remaining_amount",
+ "valid": true,
+ "q": "A sample has 250 g of fluorine-18. How much remains after 658.62 minutes?",
+ "expect": 3.90625
+ },
+ {
+ "split": "dev",
+ "kind": "remaining_amount",
+ "valid": true,
+ "q": "A substance has a half-life of 8 days. How much of 200 mg remains after 4 days?",
+ "expect": 141.4213562373095
+ },
+ {
+ "split": "dev",
+ "kind": "remaining_fraction",
+ "valid": true,
+ "q": "What fraction of radon-222 remains after 7.65 days?",
+ "expect": 0.24986407243470724
+ },
+ {
+ "split": "dev",
+ "kind": "time_to_fraction",
+ "valid": true,
+ "q": "How many years until only 50% of a sample of caesium-137 remains?",
+ "expect": 30.08
+ },
+ {
+ "split": "dev",
+ "kind": "half_lives",
+ "valid": true,
+ "q": "How many half-lives of technetium-99m pass in 90.1 hours?",
+ "expect": 14.999916759618424
+ },
+ {
+ "split": "dev",
+ "kind": "remaining_amount",
+ "valid": true,
+ "q": "A sample has 50 g of strontium-90. How much remains after 28.79 years?",
+ "expect": 25.0
+ },
+ {
+ "split": "dev",
+ "kind": "remaining_amount",
+ "valid": true,
+ "q": "A substance has a half-life of 5 days. How much of 1000 mg remains after 10 days?",
+ "expect": 250.0
+ },
+ {
+ "split": "dev",
+ "kind": "remaining_fraction",
+ "valid": true,
+ "q": "What fraction of carbon-14 remains after 5730 years?",
+ "expect": 0.5
+ },
+ {
+ "split": "dev",
+ "kind": "time_to_fraction",
+ "valid": true,
+ "q": "How many years until only 5% of a sample of radium-226 remains?",
+ "expect": 6915.08495181978
+ },
+ {
+ "split": "dev",
+ "kind": "half_lives",
+ "valid": true,
+ "q": "How many half-lives of americium-241 pass in 1946.7 years?",
+ "expect": 4.5
+ },
+ {
+ "split": "dev",
+ "kind": "remaining_amount",
+ "valid": true,
+ "q": "A sample has 800 g of carbon-14. How much remains after 5730 years?",
+ "expect": 400.0
+ },
+ {
+ "split": "dev",
+ "kind": "remaining_amount",
+ "valid": true,
+ "q": "A substance has a half-life of 12 days. How much of 40 mg remains after 4 days?",
+ "expect": 31.74802103936399
+ },
+ {
+ "split": "dev",
+ "kind": "remaining_fraction",
+ "valid": true,
+ "q": "What fraction of carbon-14 remains after 5730 years?",
+ "expect": 0.5
+ },
+ {
+ "split": "dev",
+ "kind": "time_to_fraction",
+ "valid": true,
+ "q": "How many years until only 25% of a sample of americium-241 remains?",
+ "expect": 865.2
+ },
+ {
+ "split": "dev",
+ "kind": "half_lives",
+ "valid": true,
+ "q": "How many half-lives of technetium-99m pass in 18.02 hours?",
+ "expect": 2.999983351923685
+ },
+ {
+ "split": "dev",
+ "kind": "remaining_amount",
+ "valid": true,
+ "q": "A sample has 800 g of caesium-137. How much remains after 105.28 years?",
+ "expect": 70.71067811865474
+ },
+ {
+ "split": "dev",
+ "kind": "remaining_amount",
+ "valid": true,
+ "q": "A substance has a half-life of 12 days. How much of 1000 mg remains after 30 days?",
+ "expect": 176.7766952966369
+ },
+ {
+ "split": "dev",
+ "kind": "remaining_fraction",
+ "valid": true,
+ "q": "What fraction of cobalt-60 remains after 23.72 years?",
+ "expect": 0.044201729007020775
+ },
+ {
+ "split": "dev",
+ "kind": "time_to_fraction",
+ "valid": true,
+ "q": "How many minutes until only 50% of a sample of fluorine-18 remains?",
+ "expect": 109.77
+ },
+ {
+ "split": "dev",
+ "kind": "half_lives",
+ "valid": true,
+ "q": "How many half-lives of tritium pass in 36.96 years?",
+ "expect": 3.0
+ },
+ {
+ "split": "dev",
+ "kind": "remaining_amount",
+ "valid": true,
+ "q": "A sample has 10 g of radium-226. How much remains after 9600 years?",
+ "expect": 0.15625
+ },
+ {
+ "split": "dev",
+ "kind": "remaining_amount",
+ "valid": true,
+ "q": "A substance has a half-life of 2 days. How much of 40 mg remains after 24 days?",
+ "expect": 0.009765625
+ },
+ {
+ "split": "dev",
+ "kind": "remaining_fraction",
+ "valid": true,
+ "q": "What fraction of cobalt-60 remains after 23.72 years?",
+ "expect": 0.044201729007020775
+ },
+ {
+ "split": "dev",
+ "kind": "time_to_fraction",
+ "valid": true,
+ "q": "How many years until only 50% of a sample of plutonium-239 remains?",
+ "expect": 24110.0
+ },
+ {
+ "split": "dev",
+ "kind": "half_lives",
+ "valid": true,
+ "q": "How many half-lives of caesium-137 pass in 90.24 years?",
+ "expect": 3.0
+ },
+ {
+ "split": "dev",
+ "kind": "decline",
+ "valid": false,
+ "q": "How much carbon-14 remains after 1000 years?",
+ "expect": "decline"
+ },
+ {
+ "split": "dev",
+ "kind": "decline",
+ "valid": false,
+ "q": "What fraction of iodine-131 remains?",
+ "expect": "decline"
+ },
+ {
+ "split": "heldout",
+ "kind": "remaining_amount",
+ "valid": true,
+ "q": "Starting with 250 g of phosphorus-32, how many grams are left after 7.13 days?",
+ "expect": 176.81105026553874
+ },
+ {
+ "split": "heldout",
+ "kind": "remaining_amount",
+ "valid": true,
+ "q": "An isotope's half-life is 8 days. If you begin with 40 mg, how much is still left after 10 days?",
+ "expect": 16.81792830507429
+ },
+ {
+ "split": "heldout",
+ "kind": "remaining_fraction",
+ "valid": true,
+ "q": "After 865.2 years, what fraction of the original americium-241 is still remaining?",
+ "expect": 0.25
+ },
+ {
+ "split": "heldout",
+ "kind": "time_to_fraction",
+ "valid": true,
+ "q": "For plutonium-239, how many years does it take until 1% of it remains?",
+ "expect": 160183.3727354686
+ },
+ {
+ "split": "heldout",
+ "kind": "half_lives",
+ "valid": true,
+ "q": "In 143.95 years, how many half-lives of strontium-90 have gone by?",
+ "expect": 5.0
+ },
+ {
+ "split": "heldout",
+ "kind": "remaining_amount",
+ "valid": true,
+ "q": "Starting with 800 g of tritium, how many grams are left after 24.64 years?",
+ "expect": 200.0
+ },
+ {
+ "split": "heldout",
+ "kind": "remaining_amount",
+ "valid": true,
+ "q": "An isotope's half-life is 12 days. If you begin with 200 mg, how much is still left after 30 days?",
+ "expect": 35.35533905932738
+ },
+ {
+ "split": "heldout",
+ "kind": "remaining_fraction",
+ "valid": true,
+ "q": "After 109.77 minutes, what fraction of the original fluorine-18 is still remaining?",
+ "expect": 0.5
+ },
+ {
+ "split": "heldout",
+ "kind": "time_to_fraction",
+ "valid": true,
+ "q": "For cobalt-60, how many years does it take until 10% of it remains?",
+ "expect": 17.511211759389244
+ },
+ {
+ "split": "heldout",
+ "kind": "half_lives",
+ "valid": true,
+ "q": "In 15.81 years, how many half-lives of cobalt-60 have gone by?",
+ "expect": 2.99920324771408
+ },
+ {
+ "split": "heldout",
+ "kind": "remaining_amount",
+ "valid": true,
+ "q": "Starting with 100 g of fluorine-18, how many grams are left after 658.62 minutes?",
+ "expect": 1.5625
+ },
+ {
+ "split": "heldout",
+ "kind": "remaining_amount",
+ "valid": true,
+ "q": "An isotope's half-life is 8 days. If you begin with 1000 mg, how much is still left after 30 days?",
+ "expect": 74.32544468767007
+ },
+ {
+ "split": "heldout",
+ "kind": "remaining_fraction",
+ "valid": true,
+ "q": "After 8.03 days, what fraction of the original iodine-131 is still remaining?",
+ "expect": 0.4997927517767696
+ },
+ {
+ "split": "heldout",
+ "kind": "time_to_fraction",
+ "valid": true,
+ "q": "For strontium-90, how many years does it take until 10% of it remains?",
+ "expect": 95.63830985180716
+ },
+ {
+ "split": "heldout",
+ "kind": "half_lives",
+ "valid": true,
+ "q": "In 215.92 years, how many half-lives of strontium-90 have gone by?",
+ "expect": 7.499826328586314
+ },
+ {
+ "split": "heldout",
+ "kind": "remaining_amount",
+ "valid": true,
+ "q": "Starting with 250 g of strontium-90, how many grams are left after 100.77 years?",
+ "expect": 22.094427028156776
+ },
+ {
+ "split": "heldout",
+ "kind": "remaining_amount",
+ "valid": true,
+ "q": "An isotope's half-life is 12 days. If you begin with 200 mg, how much is still left after 24 days?",
+ "expect": 50.0
+ },
+ {
+ "split": "heldout",
+ "kind": "remaining_fraction",
+ "valid": true,
+ "q": "After 72330 years, what fraction of the original plutonium-239 is still remaining?",
+ "expect": 0.125
+ },
+ {
+ "split": "heldout",
+ "kind": "time_to_fraction",
+ "valid": true,
+ "q": "For caesium-137, how many years does it take until 50% of it remains?",
+ "expect": 30.08
+ },
+ {
+ "split": "heldout",
+ "kind": "half_lives",
+ "valid": true,
+ "q": "In 135.36 years, how many half-lives of caesium-137 have gone by?",
+ "expect": 4.500000000000001
+ },
+ {
+ "split": "heldout",
+ "kind": "remaining_amount",
+ "valid": true,
+ "q": "Starting with 10 g of americium-241, how many grams are left after 216.3 years?",
+ "expect": 7.0710678118654755
+ },
+ {
+ "split": "heldout",
+ "kind": "remaining_amount",
+ "valid": true,
+ "q": "An isotope's half-life is 12 days. If you begin with 1000 mg, how much is still left after 10 days?",
+ "expect": 561.2310241546866
+ },
+ {
+ "split": "heldout",
+ "kind": "remaining_fraction",
+ "valid": true,
+ "q": "After 129.56 years, what fraction of the original strontium-90 is still remaining?",
+ "expect": 0.04418885405631354
+ },
+ {
+ "split": "heldout",
+ "kind": "time_to_fraction",
+ "valid": true,
+ "q": "For technetium-99m, how many hours does it take until 5% of it remains?",
+ "expect": 25.960525487559924
+ },
+ {
+ "split": "heldout",
+ "kind": "half_lives",
+ "valid": true,
+ "q": "In 71.34 days, how many half-lives of phosphorus-32 have gone by?",
+ "expect": 5.0
+ },
+ {
+ "split": "heldout",
+ "kind": "remaining_amount",
+ "valid": true,
+ "q": "Starting with 800 g of caesium-137, how many grams are left after 30.08 years?",
+ "expect": 400.0
+ },
+ {
+ "split": "heldout",
+ "kind": "remaining_amount",
+ "valid": true,
+ "q": "An isotope's half-life is 5 days. If you begin with 200 mg, how much is still left after 10 days?",
+ "expect": 50.0
+ },
+ {
+ "split": "heldout",
+ "kind": "remaining_fraction",
+ "valid": true,
+ "q": "After 3.82 days, what fraction of the original radon-222 is still remaining?",
+ "expect": 0.5003173512422465
+ },
+ {
+ "split": "heldout",
+ "kind": "time_to_fraction",
+ "valid": true,
+ "q": "For radon-222, how many days does it take until 50% of it remains?",
+ "expect": 3.8235
+ },
+ {
+ "split": "heldout",
+ "kind": "half_lives",
+ "valid": true,
+ "q": "In 39.54 years, how many half-lives of cobalt-60 have gone by?",
+ "expect": 7.500853663163486
+ },
+ {
+ "split": "heldout",
+ "kind": "remaining_amount",
+ "valid": true,
+ "q": "Starting with 10 g of technetium-99m, how many grams are left after 6.01 hours?",
+ "expect": 4.998096333905179
+ },
+ {
+ "split": "heldout",
+ "kind": "remaining_amount",
+ "valid": true,
+ "q": "An isotope's half-life is 5 days. If you begin with 1000 mg, how much is still left after 24 days?",
+ "expect": 35.896823593657345
+ },
+ {
+ "split": "heldout",
+ "kind": "remaining_fraction",
+ "valid": true,
+ "q": "After 24.64 years, what fraction of the original tritium is still remaining?",
+ "expect": 0.25
+ },
+ {
+ "split": "heldout",
+ "kind": "time_to_fraction",
+ "valid": true,
+ "q": "For phosphorus-32, how many days does it take until 5% of it remains?",
+ "expect": 61.665270057852894
+ },
+ {
+ "split": "heldout",
+ "kind": "half_lives",
+ "valid": true,
+ "q": "In 61.6 years, how many half-lives of tritium have gone by?",
+ "expect": 5.0
+ },
+ {
+ "split": "heldout",
+ "kind": "remaining_amount",
+ "valid": true,
+ "q": "Starting with 800 g of carbon-14, how many grams are left after 5730 years?",
+ "expect": 400.0
+ },
+ {
+ "split": "heldout",
+ "kind": "remaining_amount",
+ "valid": true,
+ "q": "An isotope's half-life is 5 days. If you begin with 200 mg, how much is still left after 30 days?",
+ "expect": 3.125
+ },
+ {
+ "split": "heldout",
+ "kind": "remaining_fraction",
+ "valid": true,
+ "q": "After 55.44 years, what fraction of the original tritium is still remaining?",
+ "expect": 0.04419417382415922
+ },
+ {
+ "split": "heldout",
+ "kind": "time_to_fraction",
+ "valid": true,
+ "q": "For strontium-90, how many years does it take until 5% of it remains?",
+ "expect": 124.42830985180717
+ },
+ {
+ "split": "heldout",
+ "kind": "half_lives",
+ "valid": true,
+ "q": "In 60.16 years, how many half-lives of caesium-137 have gone by?",
+ "expect": 2.0
+ },
+ {
+ "split": "heldout",
+ "kind": "decline",
+ "valid": false,
+ "q": "How much cobalt-60 is left after 10 years?",
+ "expect": "decline"
+ },
+ {
+ "split": "heldout",
+ "kind": "decline",
+ "valid": false,
+ "q": "What fraction of radon-222 is still remaining?",
+ "expect": "decline"
+ }
+]
\ No newline at end of file
diff --git a/root/specialists/decay-r2/decay_panel_v1.py b/root/specialists/decay-r2/decay_panel_v1.py
new file mode 100644
index 0000000000000000000000000000000000000000..b61018adedba44d2665f5f56cb1375924aaae513
--- /dev/null
+++ b/root/specialists/decay-r2/decay_panel_v1.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python3
+"""decay_panel_v1 - the sealed panel for specialist #104 (radioactive decay). Answers from the panel's OWN half-life table
+(NNDC / NuDat 3) and N = N0 (1/2)^(t/T), written here separately from the organ. Dev / held-out differ in seed, isotopes
+and phrasing (same author - disclosed). Must-decline: the time or amount missing.
+usage: decay_panel_v1.py """
+import json
+import math
+import random
+import sys
+
+HL = {"carbon-14": (5730, "years"), "cobalt-60": (5.2714, "years"), "caesium-137": (30.08, "years"), "strontium-90": (28.79, "years"),
+ "tritium": (12.32, "years"), "radium-226": (1600, "years"), "plutonium-239": (24110, "years"), "americium-241": (432.6, "years"),
+ "iodine-131": (8.0252, "days"), "radon-222": (3.8235, "days"), "phosphorus-32": (14.268, "days"),
+ "technetium-99m": (6.0067, "hours"), "fluorine-18": (109.77, "minutes")}
+T = {"dev": {"remain": "A sample has {a} g of {i}. How much remains after {t} {u}?",
+ "given": "A substance has a half-life of {h} days. How much of {a} mg remains after {t} days?",
+ "frac": "What fraction of {i} remains after {t} {u}?",
+ "until": "How many {u} until only {p}% of a sample of {i} remains?",
+ "nhl": "How many half-lives of {i} pass in {t} {u}?"},
+ "heldout": {"remain": "Starting with {a} g of {i}, how many grams are left after {t} {u}?",
+ "given": "An isotope's half-life is {h} days. If you begin with {a} mg, how much is still left after {t} days?",
+ "frac": "After {t} {u}, what fraction of the original {i} is still remaining?",
+ "until": "For {i}, how many {u} does it take until {p}% of it remains?",
+ "nhl": "In {t} {u}, how many half-lives of {i} have gone by?"}}
+DECLINE = {"dev": ["How much carbon-14 remains after 1000 years?", "What fraction of iodine-131 remains?"],
+ "heldout": ["How much cobalt-60 is left after 10 years?", "What fraction of radon-222 is still remaining?"]}
+
+
+def main():
+ items = []
+ for split, seed in (("dev", 104), ("heldout", 10404)):
+ rng = random.Random(seed)
+ t = T[split]
+ iso = sorted(HL)
+ for _ in range(8):
+ i = rng.choice(iso)
+ h, u = HL[i]
+ k = rng.choice([0.5, 1, 2, 3.5, 6])
+ a = rng.choice([10, 50, 100, 250, 800])
+ items.append({"split": split, "kind": "remaining_amount", "valid": True, "q": t["remain"].format(a=a, i=i, t=round(h * k, 2), u=u),
+ "expect": a * 0.5 ** (round(h * k, 2) / h)})
+ hd = rng.choice([2, 5, 8, 12])
+ tt = rng.choice([4, 10, 24, 30])
+ a = rng.choice([40, 200, 1000])
+ items.append({"split": split, "kind": "remaining_amount", "valid": True, "q": t["given"].format(h=hd, a=a, t=tt), "expect": a * 0.5 ** (tt / hd)})
+ i = rng.choice(iso)
+ h, u = HL[i]
+ k = rng.choice([1, 2, 3, 4.5])
+ items.append({"split": split, "kind": "remaining_fraction", "valid": True, "q": t["frac"].format(i=i, t=round(h * k, 2), u=u),
+ "expect": 0.5 ** (round(h * k, 2) / h)})
+ i = rng.choice(iso)
+ h, u = HL[i]
+ p = rng.choice([50, 25, 10, 5, 1])
+ items.append({"split": split, "kind": "time_to_fraction", "valid": True, "q": t["until"].format(u=u, p=p, i=i), "expect": h * math.log(100 / p, 2)})
+ i = rng.choice(iso)
+ h, u = HL[i]
+ tt = round(h * rng.choice([2, 3, 5, 10]) * rng.choice([1, 1.5]), 2)
+ items.append({"split": split, "kind": "half_lives", "valid": True, "q": t["nhl"].format(i=i, t=tt, u=u), "expect": tt / h})
+ for q in DECLINE[split]:
+ items.append({"split": split, "kind": "decline", "valid": False, "q": q, "expect": "decline"})
+ json.dump(items, open(sys.argv[1], "w"), indent=1)
+ import collections
+ print("panel:", dict(collections.Counter((i["split"], i["kind"]) for i in items)))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/root/specialists/decay-r2/decay_scorer.py b/root/specialists/decay-r2/decay_scorer.py
new file mode 100644
index 0000000000000000000000000000000000000000..00d82b163e46cc06bd09ddf01fa227fced2ca1de
--- /dev/null
+++ b/root/specialists/decay-r2/decay_scorer.py
@@ -0,0 +1,18 @@
+#!/usr/bin/env python3
+"""decay_scorer - the first number after "about" (opening "About ..." or "is/takes about ...") in the REPLY TEXT within 0.5%
+of the panel's own value (the panel states the unit it asks for); a must-decline item answered is a false yes. (ok, asserted)"""
+import re
+
+
+def ok(p, r):
+ text = str((r or {}).get("response", ""))
+ answered = str((r or {}).get("status", "")) == "answered"
+ if not p["valid"]:
+ return (not answered), answered
+ if not answered:
+ return False, False
+ m = re.search(r"\b[Aa]bout\s+(-?\d+(?:\.\d+)?(?:e[+-]?\d+)?)", text)
+ if not m:
+ return False, True
+ got, want = float(m.group(1)), float(p["expect"])
+ return abs(got - want) <= 0.005 * abs(want) + 1e-9, True
diff --git a/root/specialists/decay-r2/manifest.json b/root/specialists/decay-r2/manifest.json
new file mode 100644
index 0000000000000000000000000000000000000000..d79260e5ab71dff20859841fc457b7cf52e3c50f
--- /dev/null
+++ b/root/specialists/decay-r2/manifest.json
@@ -0,0 +1,65 @@
+{
+ "id": "decay-r2",
+ "name": "Oracle7 Radioactive Decay (r2: amount in the mass unit asked)",
+ "number": 104,
+ "tier": "A",
+ "sources": [
+ {
+ "name": "NNDC NuDat 3 half-lives (Brookhaven National Laboratory)",
+ "licence": "public domain (US government)",
+ "url": "https://www.nndc.bnl.gov/nudat3/"
+ },
+ {
+ "name": "Exponential decay N = N0 (1/2)^(t/T)",
+ "licence": "public domain (physical law)",
+ "url": "https://www.nndc.bnl.gov/"
+ }
+ ],
+ "store": {
+ "path": "__ORACLE7_ROOT__/specialists/decay-r2/organ_decay_r2.py"
+ },
+ "organ": {
+ "dir": "__ORACLE7_ROOT__/specialists/decay-r2",
+ "module": "organ_decay_r2",
+ "class": "DecayR2Organ",
+ "init": {}
+ },
+ "accept": {
+ "kinds": [
+ "remaining_amount",
+ "remaining_fraction",
+ "time_to_fraction",
+ "half_lives"
+ ],
+ "declines_in_domain": [
+ "underspecified"
+ ],
+ "context_regex": "\b(?:half[- ]?li(?:fe|ves)|decay\\w*|radioactive|isotope)\b"
+ },
+ "panel": {
+ "path": "__ORACLE7_ROOT__/specialists/decay-r2/decay_panel_v1.json",
+ "splits": [
+ "dev",
+ "heldout"
+ ],
+ "exclude_kinds_composed": [],
+ "scorer_module": "decay_scorer",
+ "scorer_fn": "ok"
+ },
+ "routing_cues": [
+ "half-life",
+ "remains after",
+ "how many half-lives",
+ "carbon-14",
+ "decay"
+ ],
+ "budgets": {
+ "latency_p50_ms": 50,
+ "store_mb": 1
+ },
+ "core_sample": {
+ "n": 400,
+ "seed": 20261008
+ },
+ "notes": "Half-life arithmetic for a stated half-life or a named isotope (NNDC values). Panel answers from the panel's own table, written separately; dev/held-out differ in seed and phrasing (same author). r2 (2026-10-18, sealed test C4 item c4-28): a remaining amount is converted to the mass unit the question asks for; byte-identical to decay on every panel input (invariance_v1)."
+}
\ No newline at end of file
diff --git a/root/specialists/decay-r2/narrow_v1.py b/root/specialists/decay-r2/narrow_v1.py
new file mode 100644
index 0000000000000000000000000000000000000000..647d4d5e961acd006c9de30f0ce8b0b8f5139cda
--- /dev/null
+++ b/root/specialists/decay-r2/narrow_v1.py
@@ -0,0 +1,47 @@
+#!/usr/bin/env python3
+"""narrow_v1 - the shared NARROWING rule for specialist organs (2026-09-29, from the full-core sweep):
+
+ WORD PROBLEMS AND NARRATIVE TURNS BELONG TO THE HEAD. An organ answers a direct domain question. A turn that tells a
+ story ("Justine wants to put a deck ...", "A new energy drink advertises 106 calories for 8 ounces ...") or relates
+ quantities to each other ("the length is 9 inches more than the width") is a word problem, which the core's word-
+ problem organ owns; organs firing on its nouns declined it or answered a different question.
+ An organ may DECLINE only a short direct question (it is then refusing the domain question that was asked); on
+ anything longer it says nothing and the head answers.
+
+Narrowing only ever removes turns an organ takes, so on any input where it does not fire, the organ's reply is
+byte-identical to the frozen version it wraps (checked per specialist by an invariance count before re-gating).
+"""
+import re
+
+RELATION = re.compile(r"\b(?:\d+(?:\.\d+)?|one|two|three|four|five|six|seven|eight|nine|ten)\s+(?:\w+\s+){0,2}(?:more|less|fewer|longer|shorter)\s+than\b|"
+ r"\b(?:twice|three times|half)\s+(?:as\s+\w+\s+as\s+)?the\s+\w+\b", re.I)
+
+
+# a RATE ("106 calories for 8 ounces", "3 apples per 2 dollars") makes the turn a proportion problem
+RATE = re.compile(r"\b\d+(?:\.\d+)?\s+[a-z]+\s+(?:for|per|every)\s+(?:\d+(?:\.\d+)?|a|an|one|each)\s+[a-z]+", re.I)
+NUM = re.compile(r"(?= 3 or len(t.split()) > 40 or RELATION.search(t) is not None or RATE.search(t) is not None
+
+
+def short_direct(t):
+ """a direct question an organ may decline: at most 2 sentences, at most 25 words, not narrative"""
+ t = str(t or "")
+ return sentences(t) <= 2 and len(t.split()) <= 25 and not narrative(t)
diff --git a/root/specialists/decay-r2/organ_decay_r2.py b/root/specialists/decay-r2/organ_decay_r2.py
new file mode 100644
index 0000000000000000000000000000000000000000..d57472e89a526e2b184fd041ba309cbb2f56aa9b
--- /dev/null
+++ b/root/specialists/decay-r2/organ_decay_r2.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+"""organ_decay_r2 - decay v1 with ONE change: a remaining amount is given in the mass unit the question asks for.
+
+Found by sealed composition test C4 (research record E-20261018-0034, item c4-28): asked how many MILLIGRAMS of
+caesium-137 remain from 80 g after 60.16 years, v1 said "About 20 g remains" - the right amount, in a unit the question
+did not ask for, so the number given was wrong. Now, when the asking sentence names a mass unit ("in milligrams", "how
+many kilograms") and v1's amount is in another mass unit, the amount is converted exactly (1 kg = 1,000 g = 10^6 mg =
+10^9 micrograms) and stated first, with v1's figure kept in brackets. An asked mass unit for an amount that is not a mass
+(atoms, moles, becquerels) is declined in-domain. Every other reply is v1's, byte for byte (invariance_v1 over the decay
+panel: no panel item names a target unit)."""
+from __future__ import annotations
+
+import re
+
+from narrow_v1 import last_sentence
+from organ_decay_v1 import DecayOrgan
+
+
+def g4(x):
+ """4 significant figures without an exponent (20,000 mg, not 2e+04)"""
+ if x == 0:
+ return "0"
+ if abs(x) >= 1e4:
+ return format(round(float("%.4g" % x)), ",")
+ t = "%.4g" % x
+ return ("%.12f" % x).rstrip("0").rstrip(".") if "e" in t else t
+
+MASS = {"mg": 1e-3, "milligram": 1e-3, "milligrams": 1e-3, "g": 1.0, "gram": 1.0, "grams": 1.0, "kg": 1e3, "kilogram": 1e3,
+ "kilograms": 1e3, "µg": 1e-6, "ug": 1e-6, "microgram": 1e-6, "micrograms": 1e-6}
+LABEL = {1e-3: "mg", 1.0: "g", 1e3: "kg", 1e-6: "µg"}
+ASKED = re.compile(r"\b(?:in|into|as)\s+(milligrams?|grams?|kilograms?|micrograms?|mg|kg|µg|ug|g)\b|\bhow\s+many\s+(milligrams?|grams?|kilograms?|micrograms?)\b", re.I)
+HEAD = re.compile(r"^About (\S+) (\S+) remains")
+
+
+class DecayR2Organ(DecayOrgan):
+ def answer(self, text, session_id="default"):
+ r = super().answer(text, session_id)
+ if not r or r.get("kind") != "remaining_amount" or r.get("status") != "answered":
+ return r
+ m = ASKED.search(last_sentence(text) or str(text or ""))
+ if not m:
+ return r
+ want = MASS[(m.group(1) or m.group(2)).lower()]
+ h = HEAD.match(str(r.get("response", "")))
+ if not h:
+ return r
+ have = MASS.get(h.group(2).lower())
+ if have is None:
+ return dict(r, kind="underspecified", status="withheld", value=None,
+ response="The question asks for a mass, but the amount given is in %s; I won't convert between them." % h.group(2))
+ if have == want:
+ return r
+ v = float(r["value"]) * have / want
+ out = dict(r, value=v)
+ out["response"] = HEAD.sub(lambda x: "About %s %s (%s %s) remains" % (g4(v), LABEL[want], x.group(1), x.group(2)), r["response"], count=1)
+ return out
diff --git a/root/specialists/decay-r2/organ_decay_v1.py b/root/specialists/decay-r2/organ_decay_v1.py
new file mode 100644
index 0000000000000000000000000000000000000000..c29608a883a8caa2e6d92e316b195cb3d65fda59
--- /dev/null
+++ b/root/specialists/decay-r2/organ_decay_v1.py
@@ -0,0 +1,135 @@
+#!/usr/bin/env python3
+"""organ_decay_v1 - specialist #104 (radioactive decay): how much of a sample remains after a time, how long until a given
+fraction or amount remains, and how many half-lives have passed - for a half-life the question states, or for a named
+isotope from the table below (half-lives: NNDC / NuDat 3, Brookhaven National Laboratory). N = N0 * (1/2)^(t/T).
+Formula shown. Declines a question missing the time, the amount or the half-life. Returns {kind, status, response, value}."""
+from __future__ import annotations
+
+import math
+import re
+
+from narrow_v1 import last_sentence, narrative, short_direct
+
+YEAR = 365.25 * 86400.0
+# isotope -> (half-life in seconds, how written)
+ISOTOPES = {
+ "carbon-14": (5730 * YEAR, r"carbon[- ]?14|c[- ]?14|14c|radiocarbon"), "uranium-238": (4.468e9 * YEAR, r"uranium[- ]?238|u[- ]?238"),
+ "uranium-235": (7.04e8 * YEAR, r"uranium[- ]?235|u[- ]?235"), "iodine-131": (8.0252 * 86400, r"iodine[- ]?131|i[- ]?131"),
+ "cobalt-60": (5.2714 * YEAR, r"cobalt[- ]?60|co[- ]?60"), "caesium-137": (30.08 * YEAR, r"c(?:a)?esium[- ]?137|cs[- ]?137"),
+ "strontium-90": (28.79 * YEAR, r"strontium[- ]?90|sr[- ]?90"), "radium-226": (1600 * YEAR, r"radium[- ]?226|ra[- ]?226"),
+ "plutonium-239": (24110 * YEAR, r"plutonium[- ]?239|pu[- ]?239"), "tritium": (12.32 * YEAR, r"tritium|hydrogen[- ]?3|h[- ]?3"),
+ "radon-222": (3.8235 * 86400, r"radon[- ]?222|rn[- ]?222|radon"), "potassium-40": (1.248e9 * YEAR, r"potassium[- ]?40|k[- ]?40"),
+ "phosphorus-32": (14.268 * 86400, r"phosphorus[- ]?32|p[- ]?32"), "technetium-99m": (6.0067 * 3600, r"technetium[- ]?99m|tc[- ]?99m"),
+ "fluorine-18": (109.77 * 60, r"fluorine[- ]?18|f[- ]?18"), "americium-241": (432.6 * YEAR, r"americium[- ]?241|am[- ]?241"),
+}
+ISO_RX = [(k, re.compile(r"\b(?:%s)\b" % v[1], re.I)) for k, v in ISOTOPES.items()]
+NUM = r"(\d+(?:,\d{3})*(?:\.\d+)?)"
+TUNITS = [(r"years?|yrs?", YEAR, "years"), (r"days?", 86400.0, "days"), (r"hours?|hrs?", 3600.0, "hours"),
+ (r"minutes?|mins?", 60.0, "minutes"), (r"seconds?|secs?|s\b", 1.0, "seconds")]
+TIME_RX = [(re.compile(NUM + r"\s*(?:" + u + r")(?![a-z])", re.I), f, lab) for u, f, lab in TUNITS]
+HALF_RX = re.compile(r"half[- ]?life\s+(?:of|is|was|equal to|=)?\s*(?:about\s+)?" + NUM + r"\s*(years?|yrs?|days?|hours?|hrs?|minutes?|mins?|seconds?|s)\b"
+ r"|" + NUM + r"[- ](years?|days?|hours?|minutes?|seconds?)\s+half[- ]?life", re.I)
+AMOUNT = re.compile(NUM + r"\s*(g|grams?|mg|milligrams?|kg|kilograms?|µg|micrograms?|atoms|nuclei|mol|moles?|becquerels?|bq|units|counts? per minute|cpm)\b", re.I)
+FRACTION_WORDS = {"half": 0.5, "a quarter": 0.25, "one quarter": 0.25, "an eighth": 0.125, "one eighth": 0.125,
+ "a sixteenth": 0.0625, "one sixteenth": 0.0625, "a tenth": 0.1, "one tenth": 0.1, "a hundredth": 0.01}
+
+
+def g4(x):
+ return "%.4g" % x
+
+
+def _n(s):
+ return float(s.replace(",", ""))
+
+
+def _tunit(word):
+ for u, f, lab in TUNITS:
+ if re.fullmatch(u, word, re.I):
+ return f, lab
+ return None
+
+
+class DecayOrgan:
+ def _say(self, kind, text, value=None, status="answered"):
+ return {"kind": kind, "status": status, "response": text, "value": value}
+
+ def _half_life(self, t):
+ m = HALF_RX.search(t)
+ if m:
+ v, u = (m.group(1), m.group(2)) if m.group(1) else (m.group(3), m.group(4))
+ tu = _tunit(u)
+ if tu and _n(v) > 0:
+ return _n(v) * tu[0], "a half-life of %s %s" % (g4(_n(v)), tu[1]), m.span()
+ found = [k for k, rx in ISO_RX if rx.search(t)]
+ if len(found) == 1:
+ T = ISOTOPES[found[0]][0]
+ return T, "%s (half-life %s)" % (found[0], self._fmt_t(T)), None
+ return None
+
+ @staticmethod
+ def _fmt_t(sec):
+ for u, f, lab in TUNITS:
+ if sec >= f:
+ return "%s %s" % (g4(sec / f), lab)
+ return "%s seconds" % g4(sec)
+
+ def answer(self, text, session_id="default"):
+ t = " ".join(str(text or "").split())
+ if not t or narrative(t):
+ return None
+ if not re.search(r"\bhalf[- ]?li(?:fe|ves)\b|\bdecay\w*\b|\bradioactive\b|\bisotope\b|\bremain", t, re.I):
+ return None
+ hl = self._half_life(t)
+ if hl is None:
+ return None
+ T, what, span = hl
+ rest = t if span is None else t[:span[0]] + " " + t[span[1]:]
+ ask = last_sentence(t)
+ times = [(_n(m.group(1)) * f, lab, _n(m.group(1))) for rx, f, lab in TIME_RX for m in rx.finditer(rest)]
+ amounts = AMOUNT.findall(rest)
+ frac = None
+ mf = re.search(r"\b(\d+(?:\.\d+)?)\s*(?:%|percent\b)", ask, re.I)
+ if mf:
+ frac = _n(mf.group(1)) / 100.0
+ else:
+ for w, v in FRACTION_WORDS.items():
+ if re.search(r"\b%s\b" % w, ask, re.I) and re.search(r"\bhow long|\bwhen\b|\btime\b", ask, re.I):
+ frac = v
+ break
+ # how long until a fraction (or amount) remains
+ if re.search(r"\bhow long\b|\bhow many (?:years|days|hours|minutes|seconds)\b|\bwhen\b|\btime (?:will|does|it)", ask, re.I) and not times:
+ if frac is None and len(amounts) == 2:
+ a0, a1 = _n(amounts[0][0]), _n(amounts[1][0])
+ if a0 > a1 > 0 and amounts[0][1].lower()[:2] == amounts[1][1].lower()[:2]:
+ frac = a1 / a0
+ if frac is None or not (0 < frac < 1):
+ return self._need("the fraction or amount that should remain", t)
+ tt = T * math.log(1 / frac, 2)
+ mu = re.search(r"\bhow many (years|days|hours|minutes|seconds)\b", ask, re.I)
+ shown = ("%s %s" % (g4(tt / _tunit(mu.group(1))[0]), mu.group(1).lower())) if mu else self._fmt_t(tt)
+ return self._say("time_to_fraction", "It takes about %s for %s of the sample to remain (t = T·log₂(1/f) = %s halvings of %s)." % (
+ shown, g4(frac), g4(math.log(1 / frac, 2)), what), tt)
+ if re.search(r"\bhow many half[- ]?lives\b", ask, re.I):
+ if len(times) != 1:
+ return self._need("the elapsed time", t)
+ n = times[0][0] / T
+ return self._say("half_lives", "That is about %s half-lives (t/T for %s)." % (g4(n), what), n)
+ if re.search(r"\b(?:remain\w*|left|still)\b", ask, re.I):
+ if len(times) != 1:
+ return self._need("the elapsed time", t)
+ f = 0.5 ** (times[0][0] / T)
+ if len(amounts) == 1:
+ a0, unit = _n(amounts[0][0]), amounts[0][1]
+ return self._say("remaining_amount", "About %s %s remains after %s %s (N = N₀·(1/2)^(t/T) = %s × %s, %s)." % (
+ g4(a0 * f), unit, g4(times[0][2]), times[0][1], g4(a0), g4(f), what), a0 * f)
+ if re.search(r"\b(?:fraction|percent|percentage|proportion|how much of)\b", ask, re.I) and not amounts:
+ pct = re.search(r"\bpercent|percentage\b", ask, re.I)
+ return self._say("remaining_fraction", "The remaining fraction is about %s (%s%%) after %s %s ((1/2)^(t/T), %s)." % (
+ g4(f), g4(100 * f), g4(times[0][2]), times[0][1], what), 100 * f if pct else f)
+ return self._need("the starting amount", t)
+ return None
+
+ def _need(self, what, t):
+ if not short_direct(t):
+ return None
+ return self._say("underspecified", "I need %s to compute that; I won't assume it." % what, status="withheld")
diff --git a/root/specialists/decay-r2/verdict_dev.json b/root/specialists/decay-r2/verdict_dev.json
new file mode 100644
index 0000000000000000000000000000000000000000..02de4c34cfb46774e6b0c84a6f57fb2db5d0a447
--- /dev/null
+++ b/root/specialists/decay-r2/verdict_dev.json
@@ -0,0 +1,120 @@
+{
+ "id": "decay",
+ "name": "Oracle7 Radioactive Decay",
+ "tier": "A",
+ "split": "dev",
+ "head": "oracle_release_runtime_w524",
+ "time": "2026-10-08T22:24:16Z",
+ "domain": {
+ "head": {
+ "correct": 1,
+ "false_yes": 1,
+ "wrong": 4,
+ "n": 42,
+ "p50_ms": 122.37,
+ "fails": [
+ [
+ "remaining_amount",
+ "A sample has 50 g of americium-241. How much remains after 432.6 years",
+ "act:clarify|vessel:receipt",
+ "I didn't fully parse that, so I won't guess. I can summarize a topic, compare two things, "
+ ],
+ [
+ "remaining_amount",
+ "A substance has a half-life of 12 days. How much of 40 mg remains afte",
+ "act:clarify|vessel:receipt",
+ "I didn't fully parse that, so I won't guess. I can summarize a topic, compare two things, "
+ ],
+ [
+ "remaining_fraction",
+ "What fraction of technetium-99m remains after 12.01 hours?",
+ "act:clarify|vessel:receipt",
+ "I didn't fully parse that, so I won't guess. I can summarize a topic, compare two things, "
+ ],
+ [
+ "time_to_fraction",
+ "How many years until only 50% of a sample of tritium remains?",
+ "act:clarify|vessel:receipt",
+ "I didn't fully parse that, so I won't guess. I can summarize a topic, compare two things, "
+ ],
+ [
+ "half_lives",
+ "How many half-lives of carbon-14 pass in 11460 years?",
+ "act:clarify|vessel:receipt",
+ "I didn't fully parse that, so I won't guess. I can summarize a topic, compare two things, "
+ ],
+ [
+ "remaining_amount",
+ "A sample has 100 g of caesium-137. How much remains after 15.04 years?",
+ "act:clarify|vessel:receipt",
+ "I didn't fully parse that, so I won't guess. I can summarize a topic, compare two things, "
+ ],
+ [
+ "remaining_amount",
+ "A substance has a half-life of 8 days. How much of 40 mg remains after",
+ "act:clarify|vessel:receipt",
+ "I didn't fully parse that, so I won't guess. I can summarize a topic, compare two things, "
+ ],
+ [
+ "remaining_fraction",
+ "What fraction of radon-222 remains after 11.47 days?",
+ "act:clarify|vessel:receipt",
+ "I didn't fully parse that, so I won't guess. I can summarize a topic, compare two things, "
+ ]
+ ]
+ },
+ "spec": {
+ "correct": 41,
+ "false_yes": 1,
+ "wrong": 0,
+ "n": 42,
+ "p50_ms": 0.07,
+ "fails": [
+ [
+ "decline",
+ "How much carbon-14 remains after 1000 years?",
+ "specialist_call>specialist:geology>fract",
+ "The fraction remaining is 0.8861 (88.6062%; (1/2)^(1000 years / 5730 years); carbon-14, 5,"
+ ]
+ ]
+ }
+ },
+ "core": {
+ "head": {
+ "correct": 149,
+ "false_yes": 0
+ },
+ "spec": {
+ "correct": 149,
+ "false_yes": 0
+ },
+ "n": 400,
+ "swept_in": 0,
+ "taken_by_specialist": []
+ },
+ "self": [
+ {
+ "q": "Who are you?",
+ "same": true
+ },
+ {
+ "q": "What are you?",
+ "same": true
+ },
+ {
+ "q": "Are you a human?",
+ "same": true
+ },
+ {
+ "q": "What do you value?",
+ "same": true
+ },
+ {
+ "q": "Are you conscious?",
+ "same": true
+ }
+ ],
+ "store_mb": 0.01,
+ "decision": "DEV-PASS",
+ "reasons": []
+}
\ No newline at end of file
diff --git a/root/specialists/decay-r2/verdict_heldout.json b/root/specialists/decay-r2/verdict_heldout.json
new file mode 100644
index 0000000000000000000000000000000000000000..ce46f07d75d96e9018d2e3765cb1307e995051b0
--- /dev/null
+++ b/root/specialists/decay-r2/verdict_heldout.json
@@ -0,0 +1,162 @@
+{
+ "id": "decay",
+ "name": "Oracle7 Radioactive Decay",
+ "tier": "A",
+ "split": "heldout",
+ "head": "oracle_release_runtime_w524",
+ "time": "2026-10-08T22:29:58Z",
+ "domain": {
+ "head": {
+ "correct": 2,
+ "false_yes": 0,
+ "wrong": 1,
+ "n": 42,
+ "p50_ms": 125.85,
+ "fails": [
+ [
+ "remaining_amount",
+ "Starting with 250 g of phosphorus-32, how many grams are left after 7.",
+ "arb:timeline->capability:timeline|vessel",
+ "A timeline needs at least two dated statements; the admitted material on 250 contains 0.\n\n"
+ ],
+ [
+ "remaining_amount",
+ "An isotope's half-life is 8 days. If you begin with 40 mg, how much is",
+ "arb:timeline->capability:timeline|vessel",
+ "A timeline needs at least two dated statements; the admitted material on half-life contain"
+ ],
+ [
+ "remaining_fraction",
+ "After 865.2 years, what fraction of the original americium-241 is stil",
+ "act:clarify|vessel:receipt",
+ "I didn't fully parse that, so I won't guess. I can summarize a topic, compare two things, "
+ ],
+ [
+ "time_to_fraction",
+ "For plutonium-239, how many years does it take until 1% of it remains?",
+ "act:clarify_specific",
+ "What does \"it\" refer to? Nothing earlier in this conversation names it. Tell me the name."
+ ],
+ [
+ "half_lives",
+ "In 143.95 years, how many half-lives of strontium-90 have gone by?",
+ "act:clarify|vessel:receipt",
+ "I didn't fully parse that, so I won't guess. I can summarize a topic, compare two things, "
+ ],
+ [
+ "remaining_amount",
+ "Starting with 800 g of tritium, how many grams are left after 24.64 ye",
+ "arb:timeline->capability:timeline|vessel",
+ "A timeline needs at least two dated statements; the admitted material on 800 contains 0.\n\n"
+ ],
+ [
+ "remaining_amount",
+ "An isotope's half-life is 12 days. If you begin with 200 mg, how much ",
+ "arb:timeline->capability:timeline|vessel",
+ "A timeline needs at least two dated statements; the admitted material on half-life contain"
+ ],
+ [
+ "remaining_fraction",
+ "After 109.77 minutes, what fraction of the original fluorine-18 is sti",
+ "act:clarify|vessel:receipt",
+ "I didn't fully parse that, so I won't guess. I can summarize a topic, compare two things, "
+ ]
+ ]
+ },
+ "spec": {
+ "correct": 34,
+ "false_yes": 0,
+ "wrong": 1,
+ "n": 42,
+ "p50_ms": 0.22,
+ "fails": [
+ [
+ "remaining_amount",
+ "Starting with 250 g of phosphorus-32, how many grams are left after 7.",
+ "arb:timeline->capability:timeline|vessel",
+ "A timeline needs at least two dated statements; the admitted material on 250 contains 0.\n\n"
+ ],
+ [
+ "remaining_amount",
+ "Starting with 800 g of tritium, how many grams are left after 24.64 ye",
+ "arb:timeline->capability:timeline|vessel",
+ "A timeline needs at least two dated statements; the admitted material on 800 contains 0.\n\n"
+ ],
+ [
+ "remaining_amount",
+ "Starting with 100 g of fluorine-18, how many grams are left after 658.",
+ "arb:timeline->capability:timeline|vessel",
+ "A timeline needs at least two dated statements; the admitted material on 100 contains 1.\n\n"
+ ],
+ [
+ "remaining_amount",
+ "Starting with 250 g of strontium-90, how many grams are left after 100",
+ "arb:timeline->capability:timeline|vessel",
+ "A timeline needs at least two dated statements; the admitted material on 250 contains 0.\n\n"
+ ],
+ [
+ "remaining_amount",
+ "Starting with 10 g of americium-241, how many grams are left after 216",
+ "arb:timeline->capability:timeline|vessel",
+ "A timeline needs at least two dated statements; the admitted material on 10 contains 0.\n\nY"
+ ],
+ [
+ "remaining_amount",
+ "Starting with 800 g of caesium-137, how many grams are left after 30.0",
+ "arb:timeline->capability:timeline|vessel",
+ "A timeline needs at least two dated statements; the admitted material on 800 contains 0.\n\n"
+ ],
+ [
+ "remaining_amount",
+ "Starting with 10 g of technetium-99m, how many grams are left after 6.",
+ "arb:timeline->capability:timeline|vessel",
+ "A timeline needs at least two dated statements; the admitted material on 10 contains 0.\n\nY"
+ ],
+ [
+ "remaining_amount",
+ "Starting with 800 g of carbon-14, how many grams are left after 5730 y",
+ "specialist_call>specialist:geology>fract",
+ "The fraction remaining is 0.5 (50%; (1/2)^(5730 years / 5730 years); carbon-14, 5,730 year"
+ ]
+ ]
+ }
+ },
+ "core": {
+ "head": {
+ "correct": 149,
+ "false_yes": 0
+ },
+ "spec": {
+ "correct": 149,
+ "false_yes": 0
+ },
+ "n": 400,
+ "swept_in": 0,
+ "taken_by_specialist": []
+ },
+ "self": [
+ {
+ "q": "Who are you?",
+ "same": true
+ },
+ {
+ "q": "What are you?",
+ "same": true
+ },
+ {
+ "q": "Are you a human?",
+ "same": true
+ },
+ {
+ "q": "What do you value?",
+ "same": true
+ },
+ {
+ "q": "Are you conscious?",
+ "same": true
+ }
+ ],
+ "store_mb": 0.01,
+ "decision": "ADMIT",
+ "reasons": []
+}
\ No newline at end of file
diff --git a/root/specialists/exoorbits/exoorbits_panel_v1.json b/root/specialists/exoorbits/exoorbits_panel_v1.json
new file mode 100644
index 0000000000000000000000000000000000000000..9b26b1989cf31fd30663350ccf0dd28e64cef6d4
--- /dev/null
+++ b/root/specialists/exoorbits/exoorbits_panel_v1.json
@@ -0,0 +1,443 @@
+[
+ {
+ "split": "dev",
+ "kind": "period",
+ "valid": true,
+ "q": "What is the orbital period of a planet 0.163 AU from a star of 1.15 solar masses?",
+ "expect": 22.41417255597134
+ },
+ {
+ "split": "dev",
+ "kind": "a",
+ "valid": true,
+ "q": "A planet orbits a star of 0.27 solar masses every 3.5 days. How far is it from the star, in AU?",
+ "expect": 0.02915903415194418
+ },
+ {
+ "split": "dev",
+ "kind": "flux",
+ "valid": true,
+ "q": "How much starlight does a planet 2.191 AU from a star of 0.6626 solar luminosities receive, compared with Earth?",
+ "expect": 0.13802783512735495
+ },
+ {
+ "split": "dev",
+ "kind": "flux",
+ "valid": true,
+ "q": "A planet orbits a star of 0.2 solar masses and 0.004166 solar luminosities every 60 days. How much starlight does it receive?",
+ "expect": 0.13539976194827213
+ },
+ {
+ "split": "dev",
+ "kind": "teq",
+ "valid": true,
+ "q": "What is the equilibrium temperature of a planet 1.353 AU from a star of 1 solar luminosities, with an albedo of 0.3?",
+ "expect": 218.86949302705085
+ },
+ {
+ "split": "dev",
+ "kind": "teq",
+ "valid": true,
+ "q": "What is the equilibrium temperature of a planet orbiting a star of 0.68 solar masses that receives 1.9 times Earth's sunlight, with an albedo of 0.3?",
+ "expect": 298.8976706158833
+ },
+ {
+ "split": "dev",
+ "kind": "period",
+ "valid": true,
+ "q": "What is the orbital period of a planet 0.2585 AU from a star of 0.68 solar masses?",
+ "expect": 58.21388577540843
+ },
+ {
+ "split": "dev",
+ "kind": "a",
+ "valid": true,
+ "q": "A planet orbits a star of 0.11 solar masses every 365 days. How far is it from the star, in AU?",
+ "expect": 0.478923324213476
+ },
+ {
+ "split": "dev",
+ "kind": "flux",
+ "valid": true,
+ "q": "How much starlight does a planet 0.1059 AU from a star of 0.06635 solar luminosities receive, compared with Earth?",
+ "expect": 5.916283913860334
+ },
+ {
+ "split": "dev",
+ "kind": "flux",
+ "valid": true,
+ "q": "A planet orbits a star of 0.15 solar masses and 0.001996 solar luminosities every 12 days. How much starlight does it receive?",
+ "expect": 0.6719108730050205
+ },
+ {
+ "split": "dev",
+ "kind": "teq",
+ "valid": true,
+ "q": "What is the equilibrium temperature of a planet 0.1461 AU from a star of 0.3973 solar luminosities, with an albedo of 0.3?",
+ "expect": 528.796611493532
+ },
+ {
+ "split": "dev",
+ "kind": "teq",
+ "valid": true,
+ "q": "What is the equilibrium temperature of a planet orbiting a star of 0.11 solar masses that receives 1.9 times Earth's sunlight, with an albedo of 0.35?",
+ "expect": 293.41097844809565
+ },
+ {
+ "split": "dev",
+ "kind": "period",
+ "valid": true,
+ "q": "What is the orbital period of a planet 0.06412 AU from a star of 0.36 solar masses?",
+ "expect": 9.883920527120617
+ },
+ {
+ "split": "dev",
+ "kind": "a",
+ "valid": true,
+ "q": "A planet orbits a star of 0.46 solar masses every 18 days. How far is it from the star, in AU?",
+ "expect": 0.10376258856598927
+ },
+ {
+ "split": "dev",
+ "kind": "flux",
+ "valid": true,
+ "q": "How much starlight does a planet 4.727 AU from a star of 3.116 solar luminosities receive, compared with Earth?",
+ "expect": 0.13945248073924493
+ },
+ {
+ "split": "dev",
+ "kind": "flux",
+ "valid": true,
+ "q": "A planet orbits a star of 1 solar masses and 1 solar luminosities every 60 days. How much starlight does it receive?",
+ "expect": 11.115234571161599
+ },
+ {
+ "split": "dev",
+ "kind": "teq",
+ "valid": true,
+ "q": "What is the equilibrium temperature of a planet 1.925 AU from a star of 0.2269 solar luminosities, with an albedo of 0.3?",
+ "expect": 126.641891165455
+ },
+ {
+ "split": "dev",
+ "kind": "teq",
+ "valid": true,
+ "q": "What is the equilibrium temperature of a planet orbiting a star of 0.68 solar masses that receives 1.1 times Earth's sunlight, with an albedo of 0.3?",
+ "expect": 260.7247751173495
+ },
+ {
+ "split": "dev",
+ "kind": "a",
+ "valid": true,
+ "q": "A planet orbits a star of 0.2 solar masses every 120 days. How far is it from the star, in AU?",
+ "expect": 0.2784436679990544
+ },
+ {
+ "split": "dev",
+ "kind": "flux",
+ "valid": true,
+ "q": "How much starlight does a planet 0.2585 AU from a star of 0.128 solar luminosities receive, compared with Earth?",
+ "expect": 1.9155296327196405
+ },
+ {
+ "split": "dev",
+ "kind": "flux",
+ "valid": true,
+ "q": "A planet orbits a star of 0.57 solar masses and 0.06635 solar luminosities every 30 days. How much starlight does it receive?",
+ "expect": 2.7032299050507813
+ },
+ {
+ "split": "dev",
+ "kind": "teq",
+ "valid": true,
+ "q": "What is the equilibrium temperature of a planet 0.163 AU from a star of 2.044 solar luminosities, with zero albedo?",
+ "expect": 824.301406558885
+ },
+ {
+ "split": "dev",
+ "kind": "teq",
+ "valid": true,
+ "q": "What is the equilibrium temperature of a planet orbiting a star of 0.36 solar masses that receives 1.1 times Earth's sunlight, with an albedo of 0.2?",
+ "expect": 269.5754186143804
+ },
+ {
+ "split": "dev",
+ "kind": "period",
+ "valid": true,
+ "q": "What is the orbital period of a planet 1.451 AU from a star of 0.68 solar masses?",
+ "expect": 774.1709786863621
+ },
+ {
+ "split": "dev",
+ "kind": "flux",
+ "valid": true,
+ "q": "How much starlight does a planet 0.1465 AU from a star of 0.000918 solar luminosities receive, compared with Earth?",
+ "expect": 0.042754138079651484
+ },
+ {
+ "split": "dev",
+ "kind": "flux",
+ "valid": true,
+ "q": "A planet orbits a star of 0.15 solar masses and 0.001996 solar luminosities every 5 days. How much starlight does it receive?",
+ "expect": 2.1590365339585773
+ },
+ {
+ "split": "dev",
+ "kind": "teq",
+ "valid": true,
+ "q": "What is the equilibrium temperature of a planet 1.353 AU from a star of 1 solar luminosities, with an albedo of 0.1?",
+ "expect": 233.0619357714124
+ },
+ {
+ "split": "dev",
+ "kind": "decline",
+ "valid": false,
+ "q": "What is the orbital period of a planet 0.1 AU from its star?",
+ "expect": "decline"
+ },
+ {
+ "split": "dev",
+ "kind": "decline",
+ "valid": false,
+ "q": "What is the equilibrium temperature of a planet 0.05 AU from a star of 0.01 solar luminosities?",
+ "expect": "decline"
+ },
+ {
+ "split": "dev",
+ "kind": "decline",
+ "valid": false,
+ "q": "What is the orbital period of a planet -0.2 AU from a star of 0.4 solar masses?",
+ "expect": "decline"
+ },
+ {
+ "split": "heldout",
+ "kind": "period",
+ "valid": true,
+ "q": "How many days does a planet take to orbit a 0.68 solar mass star at a distance of 0.05578 AU (its orbital period)?",
+ "expect": 5.835170268508286
+ },
+ {
+ "split": "heldout",
+ "kind": "a",
+ "valid": true,
+ "q": "What is the distance between a 0.93 solar mass star and a planet that orbits it every 41 days?",
+ "expect": 0.2271381430523887
+ },
+ {
+ "split": "heldout",
+ "kind": "flux",
+ "valid": true,
+ "q": "Compared with Earth, how much sunlight reaches a planet orbiting a 3.116 solar luminosity star at 0.373 AU?",
+ "expect": 22.396480963709937
+ },
+ {
+ "split": "heldout",
+ "kind": "flux",
+ "valid": true,
+ "q": "What insolation does a planet get if it circles a 1 solar mass, 1 solar luminosity star once every 30 days?",
+ "expect": 28.008636021451423
+ },
+ {
+ "split": "heldout",
+ "kind": "teq",
+ "valid": true,
+ "q": "A planet with an albedo of 0.1 orbits 0.6551 AU from a 0.2269 solar luminosity star. What is its equilibrium temperature?",
+ "expect": 231.16659277060026
+ },
+ {
+ "split": "heldout",
+ "kind": "teq",
+ "valid": true,
+ "q": "If a planet around a 0.77 solar mass star gets 0.4 times Earth's sunlight and has an albedo of 0.3, what is its equilibrium temperature?",
+ "expect": 202.46461318231903
+ },
+ {
+ "split": "heldout",
+ "kind": "period",
+ "valid": true,
+ "q": "How many days does a planet take to orbit a 0.11 solar mass star at a distance of 0.04108 AU (its orbital period)?",
+ "expect": 9.169370776873192
+ },
+ {
+ "split": "heldout",
+ "kind": "a",
+ "valid": true,
+ "q": "What is the distance between a 1.06 solar mass star and a planet that orbits it every 365 days?",
+ "expect": 1.019147511369371
+ },
+ {
+ "split": "heldout",
+ "kind": "flux",
+ "valid": true,
+ "q": "Compared with Earth, how much sunlight reaches a planet orbiting a 1.362 solar luminosity star at 0.1367 AU?",
+ "expect": 72.88532227674055
+ },
+ {
+ "split": "heldout",
+ "kind": "flux",
+ "valid": true,
+ "q": "What insolation does a planet get if it circles a 0.2 solar mass, 0.004166 solar luminosity star once every 12 days?",
+ "expect": 1.1576516805864803
+ },
+ {
+ "split": "heldout",
+ "kind": "teq",
+ "valid": true,
+ "q": "A planet with an albedo of 0.5 orbits 0.07152 AU from a 0.06635 solar luminosity star. What is its equilibrium temperature?",
+ "expect": 444.1693638804682
+ },
+ {
+ "split": "heldout",
+ "kind": "teq",
+ "valid": true,
+ "q": "If a planet around a 0.93 solar mass star gets 0.4 times Earth's sunlight and has an albedo of 0.35, what is its equilibrium temperature?",
+ "expect": 198.74808703772695
+ },
+ {
+ "split": "heldout",
+ "kind": "period",
+ "valid": true,
+ "q": "How many days does a planet take to orbit a 0.27 solar mass star at a distance of 0.02943 AU (its orbital period)?",
+ "expect": 3.548899784231957
+ },
+ {
+ "split": "heldout",
+ "kind": "a",
+ "valid": true,
+ "q": "What is the distance between a 0.85 solar mass star and a planet that orbits it every 9 days?",
+ "expect": 0.08021233614286263
+ },
+ {
+ "split": "heldout",
+ "kind": "flux",
+ "valid": true,
+ "q": "Compared with Earth, how much sunlight reaches a planet orbiting a 1 solar luminosity star at 0.12 AU?",
+ "expect": 69.44444444444444
+ },
+ {
+ "split": "heldout",
+ "kind": "flux",
+ "valid": true,
+ "q": "What insolation does a planet get if it circles a 0.15 solar mass, 0.001996 solar luminosity star once every 5 days?",
+ "expect": 2.1590365339585773
+ },
+ {
+ "split": "heldout",
+ "kind": "teq",
+ "valid": true,
+ "q": "A planet with zero albedo orbits 0.1828 AU from a 0.6626 solar luminosity star. What is its equilibrium temperature?",
+ "expect": 587.3329723503341
+ },
+ {
+ "split": "heldout",
+ "kind": "teq",
+ "valid": true,
+ "q": "If a planet around a 1.25 solar mass star gets 0.8 times Earth's sunlight and has an albedo of 0.3, what is its equilibrium temperature?",
+ "expect": 240.77235853268755
+ },
+ {
+ "split": "heldout",
+ "kind": "period",
+ "valid": true,
+ "q": "How many days does a planet take to orbit a 0.2 solar mass star at a distance of 0.02645 AU (its orbital period)?",
+ "expect": 3.5132879110964708
+ },
+ {
+ "split": "heldout",
+ "kind": "a",
+ "valid": true,
+ "q": "What is the distance between a 0.11 solar mass star and a planet that orbits it every 3.5 days?",
+ "expect": 0.02161637047138486
+ },
+ {
+ "split": "heldout",
+ "kind": "flux",
+ "valid": true,
+ "q": "Compared with Earth, how much sunlight reaches a planet orbiting a 0.06635 solar luminosity star at 1.05 AU?",
+ "expect": 0.06018140589569161
+ },
+ {
+ "split": "heldout",
+ "kind": "flux",
+ "valid": true,
+ "q": "What insolation does a planet get if it circles a 1 solar mass, 1 solar luminosity star once every 60 days?",
+ "expect": 11.115234571161599
+ },
+ {
+ "split": "heldout",
+ "kind": "teq",
+ "valid": true,
+ "q": "A planet with zero albedo orbits 0.6084 AU from a 3.116 solar luminosity star. What is its equilibrium temperature?",
+ "expect": 474.0939134981895
+ },
+ {
+ "split": "heldout",
+ "kind": "teq",
+ "valid": true,
+ "q": "If a planet around a 0.85 solar mass star gets 0.4 times Earth's sunlight and has an albedo of 0.2, what is its equilibrium temperature?",
+ "expect": 209.337539283164
+ },
+ {
+ "split": "heldout",
+ "kind": "period",
+ "valid": true,
+ "q": "How many days does a planet take to orbit a 1 solar mass star at a distance of 4.02 AU (its orbital period)?",
+ "expect": 2943.9423709645707
+ },
+ {
+ "split": "heldout",
+ "kind": "a",
+ "valid": true,
+ "q": "What is the distance between a 1.25 solar mass star and a planet that orbits it every 41 days?",
+ "expect": 0.2506681133987245
+ },
+ {
+ "split": "heldout",
+ "kind": "flux",
+ "valid": true,
+ "q": "Compared with Earth, how much sunlight reaches a planet orbiting a 0.128 solar luminosity star at 0.9741 AU?",
+ "expect": 0.1348971837377946
+ },
+ {
+ "split": "heldout",
+ "kind": "flux",
+ "valid": true,
+ "q": "What insolation does a planet get if it circles a 0.68 solar mass, 0.128 solar luminosity star once every 12 days?",
+ "expect": 15.730746503723681
+ },
+ {
+ "split": "heldout",
+ "kind": "teq",
+ "valid": true,
+ "q": "A planet with an albedo of 0.1 orbits 1.353 AU from a 1 solar luminosity star. What is its equilibrium temperature?",
+ "expect": 233.0619357714124
+ },
+ {
+ "split": "heldout",
+ "kind": "teq",
+ "valid": true,
+ "q": "If a planet around a 0.11 solar mass star gets 0.8 times Earth's sunlight and has an albedo of 0.35, what is its equilibrium temperature?",
+ "expect": 236.35263919844502
+ },
+ {
+ "split": "heldout",
+ "kind": "decline",
+ "valid": false,
+ "q": "How many days does a planet take to orbit a 0.3 solar mass star?",
+ "expect": "decline"
+ },
+ {
+ "split": "heldout",
+ "kind": "decline",
+ "valid": false,
+ "q": "A planet orbits 0.2 AU from a 0.05 solar luminosity star. What is its equilibrium temperature?",
+ "expect": "decline"
+ },
+ {
+ "split": "heldout",
+ "kind": "decline",
+ "valid": false,
+ "q": "A planet with an albedo of 1.4 orbits 0.1 AU from a 0.02 solar luminosity star. What is its equilibrium temperature?",
+ "expect": "decline"
+ }
+]
\ No newline at end of file
diff --git a/root/specialists/exoorbits/manifest.json b/root/specialists/exoorbits/manifest.json
new file mode 100644
index 0000000000000000000000000000000000000000..83420a5cb36b35ae1179567d60180269a6a1b005
--- /dev/null
+++ b/root/specialists/exoorbits/manifest.json
@@ -0,0 +1,71 @@
+{
+ "id": "exoorbits",
+ "name": "Oracle7 Orbits Around Stars (Kepler, starlight, equilibrium temperature)",
+ "number": 216,
+ "tier": "B",
+ "sources": [
+ {
+ "name": "Kopparapu, R. K. et al. 2013, Habitable zones around main-sequence stars: new estimates, ApJ 765, 131 (erratum values)",
+ "licence": "fact",
+ "url": "https://doi.org/10.1088/0004-637X/765/2/131"
+ },
+ {
+ "name": "IAU 2015 Resolution B3: nominal solar and planetary conversion constants (T_sun 5,772 K, R_sun 695,700 km, R_earth 6,378.1 km)",
+ "licence": "fact",
+ "url": "https://www.iau.org/static/resolutions/IAU2015_English.pdf"
+ },
+ {
+ "name": "Stefan-Boltzmann law, Kepler's third law, inverse-square law, transit geometry (Winn 2010, 'Transits and Occultations')",
+ "licence": "fact",
+ "url": "https://arxiv.org/abs/1001.2010"
+ }
+ ],
+ "store": {
+ "path": "__ORACLE7_ROOT__/specialists/exoorbits/organ_exoorbits_v1.py"
+ },
+ "organ": {
+ "dir": "__ORACLE7_ROOT__/specialists/exoorbits",
+ "module": "organ_exoorbits_v1",
+ "class": "ExoorbitsOrgan",
+ "init": {}
+ },
+ "accept": {
+ "kinds": [
+ "period",
+ "a",
+ "flux",
+ "teq"
+ ],
+ "declines_in_domain": [
+ "underspecified",
+ "out_of_range"
+ ],
+ "context_regex": "\\b(?:orbit\\w*|planet|exoplanet\\w*)\\b"
+ },
+ "panel": {
+ "path": "__ORACLE7_ROOT__/specialists/exoorbits/exoorbits_panel_v1.json",
+ "splits": [
+ "dev",
+ "heldout"
+ ],
+ "exclude_kinds_composed": [],
+ "scorer_module": "star_scorer_v1",
+ "scorer_fn": "ok"
+ },
+ "routing_cues": [
+ "orbital period around a star",
+ "solar masses",
+ "how much starlight",
+ "equilibrium temperature"
+ ],
+ "budgets": {
+ "latency_p50_ms": 50,
+ "store_mb": 1
+ },
+ "core_sample": {
+ "n": 400,
+ "seed": 20261018
+ },
+ "notes": "P^2 = a^3 / M (years, AU, solar masses), S = L / a^2, T_eq = 278.3 K S^(1/4) (1 - A)^(1/4) only with a stated albedo. Orbits of named Solar System bodies stay with #18 orbits.",
+ "built_for": "the habitable-zone transit search on the research record: each star's reasoning routed through Oracle7 (2026-10-18)"
+}
diff --git a/root/specialists/exoorbits/narrow_v1.py b/root/specialists/exoorbits/narrow_v1.py
new file mode 100644
index 0000000000000000000000000000000000000000..647d4d5e961acd006c9de30f0ce8b0b8f5139cda
--- /dev/null
+++ b/root/specialists/exoorbits/narrow_v1.py
@@ -0,0 +1,47 @@
+#!/usr/bin/env python3
+"""narrow_v1 - the shared NARROWING rule for specialist organs (2026-09-29, from the full-core sweep):
+
+ WORD PROBLEMS AND NARRATIVE TURNS BELONG TO THE HEAD. An organ answers a direct domain question. A turn that tells a
+ story ("Justine wants to put a deck ...", "A new energy drink advertises 106 calories for 8 ounces ...") or relates
+ quantities to each other ("the length is 9 inches more than the width") is a word problem, which the core's word-
+ problem organ owns; organs firing on its nouns declined it or answered a different question.
+ An organ may DECLINE only a short direct question (it is then refusing the domain question that was asked); on
+ anything longer it says nothing and the head answers.
+
+Narrowing only ever removes turns an organ takes, so on any input where it does not fire, the organ's reply is
+byte-identical to the frozen version it wraps (checked per specialist by an invariance count before re-gating).
+"""
+import re
+
+RELATION = re.compile(r"\b(?:\d+(?:\.\d+)?|one|two|three|four|five|six|seven|eight|nine|ten)\s+(?:\w+\s+){0,2}(?:more|less|fewer|longer|shorter)\s+than\b|"
+ r"\b(?:twice|three times|half)\s+(?:as\s+\w+\s+as\s+)?the\s+\w+\b", re.I)
+
+
+# a RATE ("106 calories for 8 ounces", "3 apples per 2 dollars") makes the turn a proportion problem
+RATE = re.compile(r"\b\d+(?:\.\d+)?\s+[a-z]+\s+(?:for|per|every)\s+(?:\d+(?:\.\d+)?|a|an|one|each)\s+[a-z]+", re.I)
+NUM = re.compile(r"(?= 3 or len(t.split()) > 40 or RELATION.search(t) is not None or RATE.search(t) is not None
+
+
+def short_direct(t):
+ """a direct question an organ may decline: at most 2 sentences, at most 25 words, not narrative"""
+ t = str(t or "")
+ return sentences(t) <= 2 and len(t.split()) <= 25 and not narrative(t)
diff --git a/root/specialists/exoorbits/organ_exoorbits_v1.py b/root/specialists/exoorbits/organ_exoorbits_v1.py
new file mode 100644
index 0000000000000000000000000000000000000000..30789bb2d23e844b82d0f950cbfdb83b267e58c4
--- /dev/null
+++ b/root/specialists/exoorbits/organ_exoorbits_v1.py
@@ -0,0 +1,96 @@
+#!/usr/bin/env python3
+"""organ_exoorbits_v1 - specialist #216, a planet's orbit around a star other than the Sun, from the star's mass and
+luminosity (solar units), circular orbits, the planet's own mass neglected:
+ period P = sqrt(a^3 / M) years (Kepler's third law), a in AU, M in solar masses; 1 year = 365.25 days
+ distance a = (M P^2)^(1/3) AU
+ starlight S = L / a^2 times what Earth receives (inverse-square law)
+ equilibrium T T_eq = 278.3 K x S^(1/4) x (1 - A)^(1/4), only when the albedo A is given (zero albedo said in so many
+ words counts); 278.3 K = 5,772 K x sqrt(R_sun / 2 AU)
+What is ASKED is read from the asking sentence; givens by unit (starlib_v1). Orbits of named Solar System bodies belong
+to #18 orbits; a turn naming Earth, the Moon, Mars, Jupiter or Venus as the body orbited is left to it.
+Declines in-domain: a needed mass, luminosity, distance, period or albedo missing; any amount zero or negative; an albedo
+outside 0-1; a quantity given twice. Returns {kind, status, response, value}."""
+from __future__ import annotations
+
+import re
+
+from narrow_v1 import last_sentence, narrative, short_direct
+from starlib_v1 import TEQ_EARTHFLUX, asked, fmt, kepler_a_au, kepler_period_days, one, period_days, read, twice
+
+CONTEXT = re.compile(r"\borbit\w*\b|\bstar\b|\bexoplanet|\bplanet\b", re.I)
+STAR = re.compile(r"\bsolar[\s-]+mass|\bsolar\s+luminosit|\bm[_ ]?sun\b|\bl[_ ]?sun\b|times\s+(?:the\s+)?sun'?s\s+(?:mass|luminosity)|\bof\s+the\s+sun\b", re.I)
+NAMED = re.compile(r"\b(?:around|orbiting|orbits?|circling)\s+(?:the\s+)?(?:earth|moon|mars|jupiter|venus|saturn|mercury)\b", re.I)
+ASK = {
+ "period": re.compile(r"\b(?:orbital\s+)?period\b|\bhow\s+long\b|\bhow\s+many\s+days\b|\byear\s+(?:last|is)\b|\blength\s+of\s+(?:a|its|the)\s+year\b", re.I),
+ "a": re.compile(r"\bhow\s+far\b|\bdistance\b|\bsemi-?major\s+axis\b|\borbital\s+radius\b|\bseparation\b", re.I),
+ "flux": re.compile(r"\bsunlight\b|\bstarlight\b|\binsolation\b|\b(?:stellar\s+)?flux\b|\bhow\s+much\s+light\b|\birradiation\b", re.I),
+ "teq": re.compile(r"\bequilibrium\s+temperature\b|\btemperature\b|\bhow\s+(?:hot|warm|cold)\b", re.I),
+}
+SOFT = re.compile(r"\d[\d,.]*\s+times\s+(?:the\s+)?(?:sun|earth)'?s\s+|\d[\d,.]*\s+times\s+the\s+\w+\s+of\s+the\s+sun", re.I)
+
+
+class ExoorbitsOrgan:
+ def _say(self, kind, text, value=None, status="answered"):
+ return {"kind": kind, "status": status, "response": text, "value": value}
+
+ def _no(self, text):
+ return self._say("underspecified", text, status="withheld")
+
+ def answer(self, text, session_id="default"):
+ t = " ".join(str(text or "").split())
+ if narrative(SOFT.sub("1 ", t)):
+ return None
+ r = self._answer(t)
+ if r and r.get("status") != "answered" and not short_direct(SOFT.sub("1 ", t)):
+ return None
+ return r
+
+ def _answer(self, t):
+ if not CONTEXT.search(t) or not STAR.search(t) or NAMED.search(t):
+ return None
+ if re.search(r"\bhabitable\s+zone\b|\btransit\w*\b|\bdepth\b|\bppm\b", t, re.I):
+ return None # #215 habzone and #217 transits own these
+ ask = last_sentence(t) or t
+ want = asked(ask, ASK)
+ if want is None:
+ return None
+ q = read(t)
+ if any(twice(q, k) for k in ("mstar", "lum", "au", "flux", "albedo")) or (len(q.get("days", [])) + len(q.get("years", []))) > 1:
+ return self._no("The question gives the same quantity twice, so I can't tell which value to use; give each once.")
+ M, L, a, S, A, P = one(q, "mstar"), one(q, "lum"), one(q, "au"), one(q, "flux"), one(q, "albedo"), period_days(q)
+ if any(v is not None and v <= 0 for v in (M, L, a, S, P)):
+ return self._no("A mass, luminosity, distance, period or share of starlight must be more than zero.")
+ if A is not None and not 0 <= A < 1:
+ return self._no("An albedo is a fraction from 0 to 1; %s is not one." % fmt(A))
+ if want == "period":
+ if a is None or M is None:
+ return self._no("I need the planet's distance (in AU) and the star's mass (in solar masses) for its orbital period; I won't assume either.")
+ v = kepler_period_days(a, M)
+ return self._say("period", "The orbital period is %s days (Kepler's third law, P = √(a³ / M) years: a = %s AU, M = %s solar masses; 1 year = 365.25 days, circular orbit, planet mass neglected)." % (
+ fmt(v), fmt(a), fmt(M)), v)
+ if want == "a":
+ if P is None or M is None:
+ return self._no("I need the orbital period and the star's mass (in solar masses) for the planet's distance; I won't assume either.")
+ v = kepler_a_au(P, M)
+ return self._say("a", "The planet's orbital distance is %s AU (Kepler's third law, a = (M P²)^⅓: P = %s days, M = %s solar masses; circular orbit)." % (
+ fmt(v), fmt(P), fmt(M)), v)
+ if S is None:
+ if L is None:
+ return self._no("I need the star's luminosity (in solar luminosities) to work out the starlight; I won't assume it.")
+ src = ""
+ if a is None:
+ if P is None or M is None:
+ return self._no("I need the planet's distance (in AU), or its period and the star's mass, to work out its starlight; I won't assume them.")
+ a = kepler_a_au(P, M)
+ src = "a %s-day orbit around %s solar masses is %s AU; " % (fmt(P), fmt(M), fmt(a))
+ S = L / a ** 2
+ how = "%sS = L / a²: L = %s solar luminosities, a = %s AU" % (src, fmt(L), fmt(a))
+ else:
+ how = "given"
+ if want == "flux":
+ return self._say("flux", "The planet receives %s times the starlight Earth gets from the Sun (%s; inverse-square law)." % (fmt(S), how), S)
+ if A is None:
+ return self._no("An equilibrium temperature depends on the planet's albedo; give it (for example 0.3, or zero albedo) and I'll work it out. I won't assume one.")
+ v = TEQ_EARTHFLUX * S ** 0.25 * (1 - A) ** 0.25
+ return self._say("teq", "The planet's equilibrium temperature is %s K (T = %s K × S^¼ × (1 − A)^¼: S = %s times Earth's starlight (%s), albedo A = %s; no greenhouse warming)." % (
+ fmt(v), fmt(TEQ_EARTHFLUX), fmt(S), how, fmt(A)), v)
diff --git a/root/specialists/exoorbits/star_scorer_v1.py b/root/specialists/exoorbits/star_scorer_v1.py
new file mode 100644
index 0000000000000000000000000000000000000000..69739747b5b86897413c6c1bb409ed9f4b8f7004
--- /dev/null
+++ b/root/specialists/exoorbits/star_scorer_v1.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+"""star_scorer_v1 - the scorer for the four star specialists (#214-217; one copy in each folder). Returns (good, asserted).
+A must-decline item (valid: false) is good only when the reply does not answer (an answer is a false yes).
+A valid item is good when the reply answers and:
+ - if the item has "yes" (True/False), the reply opens with that Yes/No;
+ - the reply's RESULT numbers - its numbers with every number the question gave removed (restated givens) - begin with
+ the item's expected values, in order, each within 0.5% (relative; 1e-6 absolute floor).
+Expected values are computed by the panel generator's own formulas, not by the organ."""
+import re
+
+NUM = re.compile(r"-?\d[\d,]*(?:\.\d+)?(?:[eE][+-]?\d+)?")
+
+
+def nums(t):
+ out = []
+ for m in NUM.finditer(str(t or "")):
+ try:
+ out.append(float(m.group(0).replace(",", "")))
+ except ValueError:
+ pass
+ return out
+
+
+def ok(p, r):
+ text = str((r or {}).get("response", ""))
+ answered = str((r or {}).get("status", "")) == "answered"
+ if not p.get("valid", True):
+ return (not answered), answered
+ if not answered:
+ return False, False
+ if "yes" in p:
+ if not re.match(r"\W*%s\b" % ("yes" if p["yes"] else "no"), text, re.I):
+ return False, True
+ given = nums(p["q"])
+ res = [x for x in nums(text) if not any(abs(x - g) <= 1e-9 * max(1.0, abs(g)) for g in given)]
+ want = p["expect"] if isinstance(p["expect"], list) else [p["expect"]]
+ if len(res) < len(want):
+ return False, True
+ return all(abs(x - w) <= 0.005 * abs(w) + 1e-6 for x, w in zip(res, want)), True
diff --git a/root/specialists/exoorbits/starlib_v1.py b/root/specialists/exoorbits/starlib_v1.py
new file mode 100644
index 0000000000000000000000000000000000000000..c528da249827b4ebdfa9a65433c9e5122f82c4de
--- /dev/null
+++ b/root/specialists/exoorbits/starlib_v1.py
@@ -0,0 +1,137 @@
+#!/usr/bin/env python3
+"""starlib_v1 - the quantity reader shared by the four star specialists (#214 stars, #215 habzone, #216 exoorbits,
+#217 transits; one byte-identical copy in each specialist's folder, so each loads on its own).
+
+Every quantity is read by its UNIT and ROLE, never by the order numbers appear in (factory law 2026-09-29):
+ star temperature "3,400 K", "3400 kelvin"
+ star luminosity "0.02 solar luminosities", "0.02 L_sun", "0.02 times the Sun's luminosity"
+ star radius "0.3 solar radii", "0.3 R_sun", "0.3 times the Sun's radius" / "the radius of the Sun"
+ star mass "0.4 solar masses", "0.4 M_sun", "0.4 times the Sun's mass" / "the mass of the Sun"
+ distance "0.12 AU", "0.12 astronomical units"
+ orbital period "20 days", "20 d", "1.5 years"
+ planet radius "1.6 Earth radii", "1.6 R_earth", "1.6 times Earth's radius"
+ transit depth "1,200 ppm", "0.12%", "0.12 percent", "a depth of 0.0012"
+ starlight "0.9 times Earth's sunlight / insolation / flux" (S, Earth = 1)
+ albedo "albedo of 0.3", "zero albedo", "no albedo" (never assumed)
+Constants (IAU 2015 nominal values): Sun Teff 5,772 K, R_sun 695,700 km, R_earth 6,378.1 km (equatorial), 1 AU =
+149,597,870.7 km, so 1 AU = 215.032 solar radii and R_sun = 109.076 Earth radii; 1 year = 365.25 days."""
+import math
+import re
+
+TSUN = 5772.0
+AU_IN_RSUN = 149597870.7 / 695700.0 # 215.032
+RSUN_IN_REARTH = 695700.0 / 6378.1 # 109.076
+YEAR_DAYS = 365.25
+TEQ_EARTHFLUX = TSUN * math.sqrt(1.0 / (2.0 * AU_IN_RSUN)) # 278.3 K: equilibrium temperature at S = 1, zero albedo
+
+N = r"((? 1
+
+
+def depth_fraction(q):
+ """transit depth as a fraction (ppm, percent or a bare fraction after 'depth'); None if absent or given twice"""
+ got = [x / 1e6 for x in q.get("ppm", [])] + [x / 100.0 for x in q.get("pct", [])] + q.get("frac", [])
+ return got[0] if len(got) == 1 else None
+
+
+def period_days(q):
+ d, y = q.get("days", []), q.get("years", [])
+ got = d + [v * YEAR_DAYS for v in y]
+ return got[0] if len(got) == 1 else None
+
+
+# ---------------------------------------------------------------- the physics, one place
+def luminosity(rstar, teff):
+ """L / L_sun = (R / R_sun)^2 (T / 5772 K)^4 (Stefan-Boltzmann)"""
+ return rstar ** 2 * (teff / TSUN) ** 4
+
+
+def kepler_period_days(a_au, mstar):
+ """P = sqrt(a^3 / M) years, planet mass neglected"""
+ return YEAR_DAYS * math.sqrt(a_au ** 3 / mstar)
+
+
+def kepler_a_au(p_days, mstar):
+ return (mstar * (p_days / YEAR_DAYS) ** 2) ** (1.0 / 3.0)
+
+
+CUE = re.compile(r"\b(?:what|what's|which|find|calculate|compute|estimate|determine|work\s+out|give|how|tell\s+me)\b", re.I)
+
+
+def asked(sentence, wants):
+ """what the asking sentence asks for: the first of `wants` ({key: regex}) that appears AFTER the first question cue
+ ("what is the temperature of a star with luminosity ..." asks the temperature); without a cue, the first one named"""
+ s = str(sentence or "")
+ m = CUE.search(s)
+ start = m.start() if m else 0
+ best = None
+ for k, rx in wants.items():
+ hit = rx.search(s, start)
+ if hit and (best is None or hit.start() < best[0]):
+ best = (hit.start(), k)
+ return best[1] if best else None
+
+
+def fmt(x, sig=4):
+ """a number for a reply: 4 significant figures, thousands separated, no exponent below 1e-4"""
+ if x == 0:
+ return "0"
+ if abs(x) >= 1e4:
+ return format(round(x), ",")
+ s = "%.*g" % (sig, x)
+ if "e" in s:
+ s = ("%.10f" % x).rstrip("0").rstrip(".")
+ return s
diff --git a/root/specialists/exoorbits/verdict_dev.json b/root/specialists/exoorbits/verdict_dev.json
new file mode 100644
index 0000000000000000000000000000000000000000..655937838e156f29571682448d0ea20824d2f824
--- /dev/null
+++ b/root/specialists/exoorbits/verdict_dev.json
@@ -0,0 +1,114 @@
+{
+ "id": "exoorbits",
+ "name": "Oracle7 Orbits Around Stars (Kepler, starlight, equilibrium temperature)",
+ "tier": "B",
+ "split": "dev",
+ "head": "oracle_release_runtime_w548",
+ "time": "2026-10-18T04:35:42Z",
+ "domain": {
+ "head": {
+ "correct": 3,
+ "false_yes": 0,
+ "wrong": 0,
+ "n": 30,
+ "p50_ms": 144.41,
+ "fails": [
+ [
+ "period",
+ "What is the orbital period of a planet 0.163 AU from a star of 1.15 so",
+ "corpus_query:property_of|vessel:receipt",
+ "No provenance-admitted sentence in the abstracts corpus answers that.\n\nI have received \"Wh"
+ ],
+ [
+ "a",
+ "A planet orbits a star of 0.27 solar masses every 3.5 days. How far is",
+ "act:clarify_specific",
+ "What does \"it\" refer to? Nothing earlier in this conversation names it. Tell me the name."
+ ],
+ [
+ "flux",
+ "How much starlight does a planet 2.191 AU from a star of 0.6626 solar ",
+ "act:clarify|vessel:receipt",
+ "I didn't fully parse that, so I won't guess. I can summarize a topic, compare two things, "
+ ],
+ [
+ "flux",
+ "A planet orbits a star of 0.2 solar masses and 0.004166 solar luminosi",
+ "act:clarify_specific",
+ "What does \"it\" refer to? Nothing earlier in this conversation names it. Tell me the name."
+ ],
+ [
+ "teq",
+ "What is the equilibrium temperature of a planet 1.353 AU from a star o",
+ "specialist_call>specialist:climate>under",
+ "I need the solar constant (W/m\u00b2) and the albedo to work that out; I won't assume it. [clim"
+ ],
+ [
+ "teq",
+ "What is the equilibrium temperature of a planet orbiting a star of 0.6",
+ "specialist_call>narrowed>silent",
+ "I can't verify an answer to that exact question, so I won't guess."
+ ],
+ [
+ "period",
+ "What is the orbital period of a planet 0.2585 AU from a star of 0.68 s",
+ "corpus_query:property_of|vessel:receipt",
+ "No provenance-admitted sentence in the abstracts corpus answers that.\n\nI have received \"Wh"
+ ],
+ [
+ "a",
+ "A planet orbits a star of 0.11 solar masses every 365 days. How far is",
+ "act:clarify_specific",
+ "What does \"it\" refer to? Nothing earlier in this conversation names it. Tell me the name."
+ ]
+ ]
+ },
+ "spec": {
+ "correct": 30,
+ "false_yes": 0,
+ "wrong": 0,
+ "n": 30,
+ "p50_ms": 0.12,
+ "fails": []
+ }
+ },
+ "core": {
+ "head": {
+ "correct": 158,
+ "false_yes": 1
+ },
+ "spec": {
+ "correct": 158,
+ "false_yes": 1
+ },
+ "n": 400,
+ "swept_in": 0,
+ "taken_by_specialist": [],
+ "head_cache_hits": 400
+ },
+ "self": [
+ {
+ "q": "Who are you?",
+ "same": true
+ },
+ {
+ "q": "What are you?",
+ "same": true
+ },
+ {
+ "q": "Are you a human?",
+ "same": true
+ },
+ {
+ "q": "What do you value?",
+ "same": true
+ },
+ {
+ "q": "Are you conscious?",
+ "same": true
+ }
+ ],
+ "store_mb": 0.01,
+ "decision": "DEV-PASS",
+ "reasons": []
+}
\ No newline at end of file
diff --git a/root/specialists/exoorbits/verdict_heldout.json b/root/specialists/exoorbits/verdict_heldout.json
new file mode 100644
index 0000000000000000000000000000000000000000..a0152de3228f8cf2113bea4249cc022fe746013c
--- /dev/null
+++ b/root/specialists/exoorbits/verdict_heldout.json
@@ -0,0 +1,114 @@
+{
+ "id": "exoorbits",
+ "name": "Oracle7 Orbits Around Stars (Kepler, starlight, equilibrium temperature)",
+ "tier": "B",
+ "split": "heldout",
+ "head": "oracle_release_runtime_w548",
+ "time": "2026-10-18T04:35:54Z",
+ "domain": {
+ "head": {
+ "correct": 3,
+ "false_yes": 0,
+ "wrong": 5,
+ "n": 33,
+ "p50_ms": 150.25,
+ "fails": [
+ [
+ "period",
+ "How many days does a planet take to orbit a 0.68 solar mass star at a ",
+ "act:clarify_specific",
+ "What does \"its\" refer to? Nothing earlier in this conversation names it. Tell me the name."
+ ],
+ [
+ "a",
+ "What is the distance between a 0.93 solar mass star and a planet that ",
+ "precision_guard:G5_universal|arb:definit",
+ "I can't verify an answer to that exact question, so I won't guess."
+ ],
+ [
+ "flux",
+ "Compared with Earth, how much sunlight reaches a planet orbiting a 3.1",
+ "specialist_after_guard>A1_time_not_asked",
+ "Sunlight takes about 8.3 minutes to reach Earth (mean distance 149.6 million km, NASA). [a"
+ ],
+ [
+ "flux",
+ "What insolation does a planet get if it circles a 1 solar mass, 1 sola",
+ "act:clarify_specific",
+ "What does \"it\" refer to? Nothing earlier in this conversation names it. Tell me the name."
+ ],
+ [
+ "teq",
+ "A planet with an albedo of 0.1 orbits 0.6551 AU from a 0.2269 solar lu",
+ "specialist_call>specialist:climate>under",
+ "I need the solar constant (W/m\u00b2) and the albedo to work that out; I won't assume it. [clim"
+ ],
+ [
+ "teq",
+ "If a planet around a 0.77 solar mass star gets 0.4 times Earth's sunli",
+ "specialist_call>narrowed>silent",
+ "I can't verify an answer to that exact question, so I won't guess."
+ ],
+ [
+ "period",
+ "How many days does a planet take to orbit a 0.11 solar mass star at a ",
+ "act:clarify_specific",
+ "What does \"its\" refer to? Nothing earlier in this conversation names it. Tell me the name."
+ ],
+ [
+ "a",
+ "What is the distance between a 1.06 solar mass star and a planet that ",
+ "precision_guard:G5_universal|arb:definit",
+ "I can't verify an answer to that exact question, so I won't guess."
+ ]
+ ]
+ },
+ "spec": {
+ "correct": 33,
+ "false_yes": 0,
+ "wrong": 0,
+ "n": 33,
+ "p50_ms": 0.11,
+ "fails": []
+ }
+ },
+ "core": {
+ "head": {
+ "correct": 158,
+ "false_yes": 1
+ },
+ "spec": {
+ "correct": 158,
+ "false_yes": 1
+ },
+ "n": 400,
+ "swept_in": 0,
+ "taken_by_specialist": [],
+ "head_cache_hits": 400
+ },
+ "self": [
+ {
+ "q": "Who are you?",
+ "same": true
+ },
+ {
+ "q": "What are you?",
+ "same": true
+ },
+ {
+ "q": "Are you a human?",
+ "same": true
+ },
+ {
+ "q": "What do you value?",
+ "same": true
+ },
+ {
+ "q": "Are you conscious?",
+ "same": true
+ }
+ ],
+ "store_mb": 0.01,
+ "decision": "ADMIT",
+ "reasons": []
+}
\ No newline at end of file
diff --git a/root/specialists/habzone/habzone_panel_v1.json b/root/specialists/habzone/habzone_panel_v1.json
new file mode 100644
index 0000000000000000000000000000000000000000..5f3d1542e0586dd8c69e307bfed2bd0d7608caa4
--- /dev/null
+++ b/root/specialists/habzone/habzone_panel_v1.json
@@ -0,0 +1,528 @@
+[
+ {
+ "split": "dev",
+ "kind": "zone",
+ "valid": true,
+ "q": "Where is the habitable zone of a 2,900 K star with 0.000918 solar luminosities?",
+ "expect": [
+ 0.024828025001989253,
+ 0.06669734513163185
+ ]
+ },
+ {
+ "split": "dev",
+ "kind": "zone",
+ "valid": true,
+ "q": "What is the habitable zone of a 5,400 K star with a radius of 0.93 solar radii?",
+ "expect": [
+ 0.6203493670082026,
+ 1.492241079668632
+ ]
+ },
+ {
+ "split": "dev",
+ "kind": "zone_period",
+ "valid": true,
+ "q": "What orbital periods does the habitable zone of a 6,600 K star with 3.116 solar luminosities and 1.25 solar masses span, in days?",
+ "expect": [
+ 475.32094759341356,
+ 1642.149887470623
+ ]
+ },
+ {
+ "split": "dev",
+ "kind": "inside",
+ "valid": true,
+ "q": "A planet orbits a 3,700 K star of 0.03419 solar luminosities at 0.1622 AU. Is it in the habitable zone?",
+ "expect": 1.2995631886468577,
+ "yes": true
+ },
+ {
+ "split": "dev",
+ "kind": "inside",
+ "valid": true,
+ "q": "Is a planet that receives 1.6 times Earth's sunlight from a 3,050 K star in the habitable zone?",
+ "expect": [],
+ "yes": false
+ },
+ {
+ "split": "dev",
+ "kind": "zone",
+ "valid": true,
+ "q": "Where is the habitable zone of a 6,300 K star with 2.044 solar luminosities?",
+ "expect": [
+ 1.0512955855028516,
+ 2.429715351274557
+ ]
+ },
+ {
+ "split": "dev",
+ "kind": "zone",
+ "valid": true,
+ "q": "What is the habitable zone of a 3,500 K star with a radius of 0.36 solar radii?",
+ "expect": [
+ 0.10726982473506817,
+ 0.28140484321790477
+ ]
+ },
+ {
+ "split": "dev",
+ "kind": "zone_period",
+ "valid": true,
+ "q": "What orbital periods does the habitable zone of a 3,350 K star with 0.008896 solar luminosities and 0.27 solar masses span, in days?",
+ "expect": [
+ 14.924505273614164,
+ 63.98345750227277
+ ]
+ },
+ {
+ "split": "dev",
+ "kind": "inside",
+ "valid": true,
+ "q": "A planet orbits a 3,050 K star of 0.001996 solar luminosities at 0.04846 AU. Is it in the habitable zone?",
+ "expect": 0.8499506636854234,
+ "yes": true
+ },
+ {
+ "split": "dev",
+ "kind": "inside",
+ "valid": true,
+ "q": "Is a planet that receives 2.4 times Earth's sunlight from a 5,400 K star in the habitable zone?",
+ "expect": [],
+ "yes": false
+ },
+ {
+ "split": "dev",
+ "kind": "zone",
+ "valid": true,
+ "q": "What is the habitable zone of a 4,600 K star with a radius of 0.75 solar radii?",
+ "expect": [
+ 0.3741719117026502,
+ 0.9354151724149957
+ ]
+ },
+ {
+ "split": "dev",
+ "kind": "zone_period",
+ "valid": true,
+ "q": "What orbital periods does the habitable zone of a 3,950 K star with 0.06635 solar luminosities and 0.57 solar masses span, in days?",
+ "expect": [
+ 45.382318652023784,
+ 187.5019879601047
+ ]
+ },
+ {
+ "split": "dev",
+ "kind": "inside",
+ "valid": true,
+ "q": "A planet orbits a 3,500 K star of 0.01752 solar luminosities at 0.1161 AU. Is it in the habitable zone?",
+ "expect": 1.2997794381124712,
+ "yes": true
+ },
+ {
+ "split": "dev",
+ "kind": "zone",
+ "valid": true,
+ "q": "What is the habitable zone of a 6,000 K star with a radius of 1.08 solar radii?",
+ "expect": [
+ 0.8681840138807946,
+ 2.031695415451728
+ ]
+ },
+ {
+ "split": "dev",
+ "kind": "zone_period",
+ "valid": true,
+ "q": "What orbital periods does the habitable zone of a 3,050 K star with 0.001996 solar luminosities and 0.15 solar masses span, in days?",
+ "expect": [
+ 6.583188852258809,
+ 28.727238387043222
+ ]
+ },
+ {
+ "split": "dev",
+ "kind": "inside",
+ "valid": true,
+ "q": "A planet orbits a 5,000 K star of 0.3973 solar luminosities at 0.425 AU. Is it in the habitable zone?",
+ "expect": 2.1995847750865054,
+ "yes": false
+ },
+ {
+ "split": "dev",
+ "kind": "inside",
+ "valid": true,
+ "q": "Is a planet that receives 1.6 times Earth's sunlight from a 5,772 K star in the habitable zone?",
+ "expect": [],
+ "yes": true
+ },
+ {
+ "split": "dev",
+ "kind": "zone",
+ "valid": true,
+ "q": "What is the habitable zone of a 3,200 K star with a radius of 0.21 solar radii?",
+ "expect": [
+ 0.052626088175047936,
+ 0.1397043992764345
+ ]
+ },
+ {
+ "split": "dev",
+ "kind": "zone_period",
+ "valid": true,
+ "q": "What orbital periods does the habitable zone of a 3,500 K star with 0.01752 solar luminosities and 0.36 solar masses span, in days?",
+ "expect": [
+ 21.38587333412739,
+ 90.86737563801083
+ ]
+ },
+ {
+ "split": "dev",
+ "kind": "inside",
+ "valid": true,
+ "q": "A planet orbits a 5,400 K star of 0.6626 solar luminosities at 0.7139 AU. Is it in the habitable zone?",
+ "expect": 1.3000997285978049,
+ "yes": true
+ },
+ {
+ "split": "dev",
+ "kind": "inside",
+ "valid": true,
+ "q": "Is a planet that receives 0.55 times Earth's sunlight from a 3,050 K star in the habitable zone?",
+ "expect": [],
+ "yes": true
+ },
+ {
+ "split": "dev",
+ "kind": "zone",
+ "valid": true,
+ "q": "Where is the habitable zone of a 4,600 K star with 0.2269 solar luminosities?",
+ "expect": [
+ 0.3741658515462391,
+ 0.9354000222605431
+ ]
+ },
+ {
+ "split": "dev",
+ "kind": "zone_period",
+ "valid": true,
+ "q": "What orbital periods does the habitable zone of a 4,250 K star with 0.128 solar luminosities and 0.68 solar masses span, in days?",
+ "expect": [
+ 67.1317020207005,
+ 271.89941348526435
+ ]
+ },
+ {
+ "split": "dev",
+ "kind": "inside",
+ "valid": true,
+ "q": "A planet orbits a 6,300 K star of 2.044 solar luminosities at 1.254 AU. Is it in the habitable zone?",
+ "expect": 1.2998277918952812,
+ "yes": true
+ },
+ {
+ "split": "dev",
+ "kind": "inside",
+ "valid": true,
+ "q": "Is a planet that receives 0.55 times Earth's sunlight from a 6,300 K star in the habitable zone?",
+ "expect": [],
+ "yes": true
+ },
+ {
+ "split": "dev",
+ "kind": "decline",
+ "valid": false,
+ "q": "Where is the habitable zone of a 9,500 K star with 30 solar luminosities?",
+ "expect": "decline"
+ },
+ {
+ "split": "dev",
+ "kind": "decline",
+ "valid": false,
+ "q": "Where is the habitable zone of a star with 0.05 solar luminosities?",
+ "expect": "decline"
+ },
+ {
+ "split": "dev",
+ "kind": "decline",
+ "valid": false,
+ "q": "What orbital periods does the habitable zone of a 3,400 K star with 0.015 solar luminosities span, in days?",
+ "expect": "decline"
+ },
+ {
+ "split": "heldout",
+ "kind": "zone",
+ "valid": true,
+ "q": "I'm looking at a 5,400 K star putting out 0.6626 solar luminosities - where does its habitable zone sit, in AU?",
+ "expect": [
+ 0.6203606406945357,
+ 1.492268198351684
+ ]
+ },
+ {
+ "split": "heldout",
+ "kind": "zone",
+ "valid": true,
+ "q": "Give the habitable zone, in AU, for a star of 0.93 solar radii at 5,400 K.",
+ "expect": [
+ 0.6203493670082026,
+ 1.492241079668632
+ ]
+ },
+ {
+ "split": "heldout",
+ "kind": "zone_period",
+ "valid": true,
+ "q": "For a 1.06 solar mass star at 6,000 K and 1.362 solar luminosities, how many days is a year at the edges of the habitable zone (orbital period)?",
+ "expect": [
+ 286.99701696730585,
+ 1027.4190321892925
+ ]
+ },
+ {
+ "split": "heldout",
+ "kind": "inside",
+ "valid": true,
+ "q": "Is a planet 0.05768 AU from a 3,050 K, 0.001996 solar luminosity star inside its habitable zone?",
+ "expect": 0.5999430595124278,
+ "yes": true
+ },
+ {
+ "split": "heldout",
+ "kind": "inside",
+ "valid": true,
+ "q": "Would a world getting 1.2 times Earth's sunlight from its 6,000 K star count as inside the habitable zone?",
+ "expect": [],
+ "yes": true
+ },
+ {
+ "split": "heldout",
+ "kind": "zone",
+ "valid": true,
+ "q": "I'm looking at a 3,700 K star putting out 0.03419 solar luminosities - where does its habitable zone sit, in AU?",
+ "expect": [
+ 0.14915615718480033,
+ 0.3881183272118105
+ ]
+ },
+ {
+ "split": "heldout",
+ "kind": "zone",
+ "valid": true,
+ "q": "Give the habitable zone, in AU, for a star of 1.2 solar radii at 6,300 K.",
+ "expect": [
+ 1.0512211622917496,
+ 2.429543347110362
+ ]
+ },
+ {
+ "split": "heldout",
+ "kind": "zone_period",
+ "valid": true,
+ "q": "For a 0.2 solar mass star at 3,200 K and 0.004166 solar luminosities, how many days is a year at the edges of the habitable zone (orbital period)?",
+ "expect": [
+ 9.859761271747155,
+ 42.64615583106788
+ ]
+ },
+ {
+ "split": "heldout",
+ "kind": "inside",
+ "valid": true,
+ "q": "Is a planet 0.2412 AU from a 4,250 K, 0.128 solar luminosity star inside its habitable zone?",
+ "expect": 2.200165562458575,
+ "yes": false
+ },
+ {
+ "split": "heldout",
+ "kind": "inside",
+ "valid": true,
+ "q": "Would a world getting 0.3 times Earth's sunlight from its 3,500 K star count as inside the habitable zone?",
+ "expect": [],
+ "yes": true
+ },
+ {
+ "split": "heldout",
+ "kind": "zone",
+ "valid": true,
+ "q": "I'm looking at a 3,500 K star putting out 0.01752 solar luminosities - where does its habitable zone sit, in AU?",
+ "expect": [
+ 0.10726515813016968,
+ 0.28139260114308956
+ ]
+ },
+ {
+ "split": "heldout",
+ "kind": "zone",
+ "valid": true,
+ "q": "Give the habitable zone, in AU, for a star of 0.16 solar radii at 3,050 K.",
+ "expect": [
+ 0.03652424331236253,
+ 0.09753330408913466
+ ]
+ },
+ {
+ "split": "heldout",
+ "kind": "zone_period",
+ "valid": true,
+ "q": "For a 0.36 solar mass star at 3,500 K and 0.01752 solar luminosities, how many days is a year at the edges of the habitable zone (orbital period)?",
+ "expect": [
+ 21.38587333412739,
+ 90.86737563801083
+ ]
+ },
+ {
+ "split": "heldout",
+ "kind": "inside",
+ "valid": true,
+ "q": "Is a planet 0.1436 AU from a 3,500 K, 0.01752 solar luminosity star inside its habitable zone?",
+ "expect": 0.8496209681799489,
+ "yes": true
+ },
+ {
+ "split": "heldout",
+ "kind": "inside",
+ "valid": true,
+ "q": "Would a world getting 0.9 times Earth's sunlight from its 3,350 K star count as inside the habitable zone?",
+ "expect": [],
+ "yes": true
+ },
+ {
+ "split": "heldout",
+ "kind": "zone",
+ "valid": true,
+ "q": "I'm looking at a 6,300 K star putting out 2.044 solar luminosities - where does its habitable zone sit, in AU?",
+ "expect": [
+ 1.0512955855028516,
+ 2.429715351274557
+ ]
+ },
+ {
+ "split": "heldout",
+ "kind": "zone",
+ "valid": true,
+ "q": "Give the habitable zone, in AU, for a star of 0.75 solar radii at 4,600 K.",
+ "expect": [
+ 0.3741719117026502,
+ 0.9354151724149957
+ ]
+ },
+ {
+ "split": "heldout",
+ "kind": "inside",
+ "valid": true,
+ "q": "Is a planet 0.1023 AU from a 3,350 K, 0.008896 solar luminosity star inside its habitable zone?",
+ "expect": 0.8500481114235725,
+ "yes": true
+ },
+ {
+ "split": "heldout",
+ "kind": "inside",
+ "valid": true,
+ "q": "Would a world getting 0.3 times Earth's sunlight from its 6,300 K star count as inside the habitable zone?",
+ "expect": [],
+ "yes": false
+ },
+ {
+ "split": "heldout",
+ "kind": "zone",
+ "valid": true,
+ "q": "I'm looking at a 3,350 K star putting out 0.008896 solar luminosities - where does its habitable zone sit, in AU?",
+ "expect": [
+ 0.0766762858036458,
+ 0.2023514129865967
+ ]
+ },
+ {
+ "split": "heldout",
+ "kind": "zone_period",
+ "valid": true,
+ "q": "For a 0.15 solar mass star at 3,050 K and 0.001996 solar luminosities, how many days is a year at the edges of the habitable zone (orbital period)?",
+ "expect": [
+ 6.583188852258809,
+ 28.727238387043222
+ ]
+ },
+ {
+ "split": "heldout",
+ "kind": "inside",
+ "valid": true,
+ "q": "Is a planet 1.266 AU from a 6,000 K, 1.362 solar luminosity star inside its habitable zone?",
+ "expect": 0.8497862431960947,
+ "yes": true
+ },
+ {
+ "split": "heldout",
+ "kind": "inside",
+ "valid": true,
+ "q": "Would a world getting 1.2 times Earth's sunlight from its 4,250 K star count as inside the habitable zone?",
+ "expect": [],
+ "yes": true
+ },
+ {
+ "split": "heldout",
+ "kind": "zone",
+ "valid": true,
+ "q": "I'm looking at a 5,000 K star putting out 0.3973 solar luminosities - where does its habitable zone sit, in AU?",
+ "expect": [
+ 0.48795667771233375,
+ 1.1966521118302016
+ ]
+ },
+ {
+ "split": "heldout",
+ "kind": "zone",
+ "valid": true,
+ "q": "Give the habitable zone, in AU, for a star of 1.35 solar radii at 6,600 K.",
+ "expect": [
+ 1.2839203851947496,
+ 2.934192851899863
+ ]
+ },
+ {
+ "split": "heldout",
+ "kind": "zone_period",
+ "valid": true,
+ "q": "For a 0.77 solar mass star at 4,600 K and 0.2269 solar luminosities, how many days is a year at the edges of the habitable zone (orbital period)?",
+ "expect": [
+ 95.26669201452714,
+ 376.5658445451621
+ ]
+ },
+ {
+ "split": "heldout",
+ "kind": "inside",
+ "valid": true,
+ "q": "Is a planet 0.03918 AU from a 3,050 K, 0.001996 solar luminosity star inside its habitable zone?",
+ "expect": 1.300264404467177,
+ "yes": true
+ },
+ {
+ "split": "heldout",
+ "kind": "inside",
+ "valid": true,
+ "q": "Would a world getting 0.3 times Earth's sunlight from its 3,050 K star count as inside the habitable zone?",
+ "expect": [],
+ "yes": true
+ },
+ {
+ "split": "heldout",
+ "kind": "decline",
+ "valid": false,
+ "q": "Give the habitable zone, in AU, for a 2,100 K star of 0.001 solar luminosities.",
+ "expect": "decline"
+ },
+ {
+ "split": "heldout",
+ "kind": "decline",
+ "valid": false,
+ "q": "Where does the habitable zone of a 4,200 K star sit?",
+ "expect": "decline"
+ },
+ {
+ "split": "heldout",
+ "kind": "decline",
+ "valid": false,
+ "q": "Is a planet -0.3 AU from a 3,500 K, 0.02 solar luminosity star inside its habitable zone?",
+ "expect": "decline"
+ }
+]
\ No newline at end of file
diff --git a/root/specialists/habzone/manifest.json b/root/specialists/habzone/manifest.json
new file mode 100644
index 0000000000000000000000000000000000000000..918c82391bd9bcd922c1e727bff1bb8bbefbfa03
--- /dev/null
+++ b/root/specialists/habzone/manifest.json
@@ -0,0 +1,69 @@
+{
+ "id": "habzone",
+ "name": "Oracle7 Habitable Zone (Kopparapu et al. 2013)",
+ "number": 215,
+ "tier": "B",
+ "sources": [
+ {
+ "name": "Kopparapu, R. K. et al. 2013, Habitable zones around main-sequence stars: new estimates, ApJ 765, 131 (erratum values)",
+ "licence": "fact",
+ "url": "https://doi.org/10.1088/0004-637X/765/2/131"
+ },
+ {
+ "name": "IAU 2015 Resolution B3: nominal solar and planetary conversion constants (T_sun 5,772 K, R_sun 695,700 km, R_earth 6,378.1 km)",
+ "licence": "fact",
+ "url": "https://www.iau.org/static/resolutions/IAU2015_English.pdf"
+ },
+ {
+ "name": "Stefan-Boltzmann law, Kepler's third law, inverse-square law, transit geometry (Winn 2010, 'Transits and Occultations')",
+ "licence": "fact",
+ "url": "https://arxiv.org/abs/1001.2010"
+ }
+ ],
+ "store": {
+ "path": "__ORACLE7_ROOT__/specialists/habzone/organ_habzone_v1.py"
+ },
+ "organ": {
+ "dir": "__ORACLE7_ROOT__/specialists/habzone",
+ "module": "organ_habzone_v1",
+ "class": "HabzoneOrgan",
+ "init": {}
+ },
+ "accept": {
+ "kinds": [
+ "zone",
+ "zone_period",
+ "inside"
+ ],
+ "declines_in_domain": [
+ "underspecified",
+ "out_of_range"
+ ],
+ "context_regex": "\\b(?:habitable|goldilocks)\\s+zone\\b|\\bliquid[- ]water\\s+zone\\b"
+ },
+ "panel": {
+ "path": "__ORACLE7_ROOT__/specialists/habzone/habzone_panel_v1.json",
+ "splits": [
+ "dev",
+ "heldout"
+ ],
+ "exclude_kinds_composed": [],
+ "scorer_module": "star_scorer_v1",
+ "scorer_fn": "ok"
+ },
+ "routing_cues": [
+ "habitable zone",
+ "goldilocks zone",
+ "in the habitable zone"
+ ],
+ "budgets": {
+ "latency_p50_ms": 50,
+ "store_mb": 1
+ },
+ "core_sample": {
+ "n": 400,
+ "seed": 20261018
+ },
+ "notes": "Edges as starlight limits polynomial in Teff (2,600-7,200 K), in AU from L (or R and Teff), as orbital periods with the star's mass; whether a planet is inside from its distance, starlight or period. Declines outside the fitted range.",
+ "built_for": "the habitable-zone transit search on the research record: each star's reasoning routed through Oracle7 (2026-10-18)"
+}
diff --git a/root/specialists/habzone/narrow_v1.py b/root/specialists/habzone/narrow_v1.py
new file mode 100644
index 0000000000000000000000000000000000000000..647d4d5e961acd006c9de30f0ce8b0b8f5139cda
--- /dev/null
+++ b/root/specialists/habzone/narrow_v1.py
@@ -0,0 +1,47 @@
+#!/usr/bin/env python3
+"""narrow_v1 - the shared NARROWING rule for specialist organs (2026-09-29, from the full-core sweep):
+
+ WORD PROBLEMS AND NARRATIVE TURNS BELONG TO THE HEAD. An organ answers a direct domain question. A turn that tells a
+ story ("Justine wants to put a deck ...", "A new energy drink advertises 106 calories for 8 ounces ...") or relates
+ quantities to each other ("the length is 9 inches more than the width") is a word problem, which the core's word-
+ problem organ owns; organs firing on its nouns declined it or answered a different question.
+ An organ may DECLINE only a short direct question (it is then refusing the domain question that was asked); on
+ anything longer it says nothing and the head answers.
+
+Narrowing only ever removes turns an organ takes, so on any input where it does not fire, the organ's reply is
+byte-identical to the frozen version it wraps (checked per specialist by an invariance count before re-gating).
+"""
+import re
+
+RELATION = re.compile(r"\b(?:\d+(?:\.\d+)?|one|two|three|four|five|six|seven|eight|nine|ten)\s+(?:\w+\s+){0,2}(?:more|less|fewer|longer|shorter)\s+than\b|"
+ r"\b(?:twice|three times|half)\s+(?:as\s+\w+\s+as\s+)?the\s+\w+\b", re.I)
+
+
+# a RATE ("106 calories for 8 ounces", "3 apples per 2 dollars") makes the turn a proportion problem
+RATE = re.compile(r"\b\d+(?:\.\d+)?\s+[a-z]+\s+(?:for|per|every)\s+(?:\d+(?:\.\d+)?|a|an|one|each)\s+[a-z]+", re.I)
+NUM = re.compile(r"(?= 3 or len(t.split()) > 40 or RELATION.search(t) is not None or RATE.search(t) is not None
+
+
+def short_direct(t):
+ """a direct question an organ may decline: at most 2 sentences, at most 25 words, not narrative"""
+ t = str(t or "")
+ return sentences(t) <= 2 and len(t.split()) <= 25 and not narrative(t)
diff --git a/root/specialists/habzone/organ_habzone_v1.py b/root/specialists/habzone/organ_habzone_v1.py
new file mode 100644
index 0000000000000000000000000000000000000000..020ef23c8bbe400e3f9e230a28cb081ecede0506
--- /dev/null
+++ b/root/specialists/habzone/organ_habzone_v1.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python3
+"""organ_habzone_v1 - specialist #215, the habitable zone of a main-sequence star, from Kopparapu et al. 2013
+(ApJ 765, 131, with its erratum): the zone's edges are limits on the starlight a planet receives (S, Earth = 1), each a
+polynomial in the star's effective temperature, S = S0 + a t + b t^2 + c t^3 + d t^4 with t = T - 5,780 K, fitted for
+2,600-7,200 K.
+ conservative zone runaway greenhouse (inner) to maximum greenhouse (outer)
+ optimistic zone recent Venus (inner) to early Mars (outer)
+ edge distance d = sqrt(L / S) AU, L in solar luminosities (or from radius and temperature, Stefan-Boltzmann)
+ edge period Kepler's third law with the star's mass, when the question asks for days or periods
+ inside or not from the planet's distance (AU), its starlight (times Earth's), or its period with the star's mass
+Declines in-domain: temperature missing or outside 2,600-7,200 K; luminosity (or radius) missing; mass missing when
+periods are asked; any amount zero or negative; a quantity given twice.
+Returns {kind, status, response, value}; value = the optimistic zone's inner edge for "zone", the S for "inside"."""
+from __future__ import annotations
+
+import math
+import re
+
+from narrow_v1 import last_sentence, narrative, short_direct
+from starlib_v1 import fmt, kepler_a_au, kepler_period_days, luminosity, one, period_days, read, twice
+
+LIMITS = {"recent Venus": (1.7753, 1.4316e-4, 2.9875e-9, -7.5702e-12, -1.1635e-15),
+ "runaway greenhouse": (1.0512, 1.3242e-4, 1.5418e-8, -7.9895e-12, -1.8328e-15),
+ "maximum greenhouse": (0.3438, 5.8942e-5, 1.6558e-9, -3.0045e-12, -5.2983e-16),
+ "early Mars": (0.3179, 5.4513e-5, 1.5313e-9, -2.7786e-12, -4.8997e-16)}
+HZ = re.compile(r"\bhabitable\s+zone\b|\bgoldilocks\s+zone\b|\bliquid[- ]water\s+zone\b", re.I)
+ASK_PERIOD = re.compile(r"\b(?:orbital\s+)?periods?\b|\bin\s+days\b|\bhow\s+many\s+days\b|\bhow\s+long\b", re.I)
+ASK_INSIDE = re.compile(r"^\s*(?:is|does|would|could|will|can)\b|\b(?:inside|within|in)\s+(?:the|its|their)\s+(?:star'?s\s+)?(?:habitable|goldilocks)", re.I)
+SOFT = re.compile(r"\d[\d,.]*\s+times\s+(?:the\s+)?(?:sun|earth)'?s\s+|\d[\d,.]*\s+times\s+the\s+\w+\s+of\s+the\s+sun", re.I)
+
+
+def seff(name, teff):
+ s0, a, b, c, d = LIMITS[name]
+ t = teff - 5780.0
+ return s0 + a * t + b * t ** 2 + c * t ** 3 + d * t ** 4
+
+
+class HabzoneOrgan:
+ def _say(self, kind, text, value=None, status="answered"):
+ return {"kind": kind, "status": status, "response": text, "value": value}
+
+ def _no(self, kind, text):
+ return self._say(kind, text, status="withheld")
+
+ def answer(self, text, session_id="default"):
+ t = " ".join(str(text or "").split())
+ if narrative(SOFT.sub("1 ", t)):
+ return None
+ r = self._answer(t)
+ if r and r.get("status") != "answered" and not short_direct(SOFT.sub("1 ", t)):
+ return None
+ return r
+
+ def _answer(self, t):
+ if not HZ.search(t):
+ return None
+ ask = last_sentence(t) or t
+ q = read(t)
+ if any(twice(q, k) for k in ("teff", "lum", "rstar", "mstar", "au", "flux")):
+ return self._no("underspecified", "The question gives the same quantity twice, so I can't tell which value to use; give each once.")
+ T, L, R, M, d, S, P = one(q, "teff"), one(q, "lum"), one(q, "rstar"), one(q, "mstar"), one(q, "au"), one(q, "flux"), period_days(q)
+ if any(v is not None and v <= 0 for v in (T, L, R, M, d, S, P)):
+ return self._no("underspecified", "A temperature, luminosity, radius, mass, distance, period or share of sunlight must be more than zero.")
+ if T is None:
+ return self._no("underspecified", "Give the star's temperature (for example 3,400 K) and I can place its habitable zone; I won't assume it.")
+ if not 2600 <= T <= 7200:
+ return self._no("out_of_range", "The habitable-zone limits I use (Kopparapu et al. 2013) are fitted for stars of 2,600-7,200 K; %s K is outside them, so I won't guess." % fmt(T))
+ lsrc = ""
+ if L is None and R is not None:
+ L = luminosity(R, T)
+ lsrc = " Luminosity from radius %s solar radii and %s K by the Stefan-Boltzmann law." % (fmt(R), fmt(T))
+ lim = {k: seff(k, T) for k in LIMITS}
+ star = "a %s K star" % fmt(T)
+ inside = bool(ASK_INSIDE.search(ask)) and (d is not None or S is not None or P is not None)
+ if inside:
+ how = ""
+ if S is None and d is None and P is not None:
+ if M is None:
+ return self._no("underspecified", "To place a planet by its period I need the star's mass (in solar masses); I won't assume it.")
+ d = kepler_a_au(P, M)
+ how = "a %s-day orbit around %s solar masses is %s AU (Kepler's third law); " % (fmt(P), fmt(M), fmt(d))
+ if S is None:
+ if L is None:
+ return self._no("underspecified", "To place a planet by its distance I need the star's luminosity (or its radius); I won't assume it.")
+ S = L / d ** 2
+ how += "%s AU from %s solar luminosities%s, S = L / d\u00b2" % (fmt(d), fmt(L), lsrc)
+ opt = lim["early Mars"] <= S <= lim["recent Venus"]
+ cons = lim["maximum greenhouse"] <= S <= lim["runaway greenhouse"]
+ where = ("inside the conservative zone" if cons else "inside the optimistic zone only") if opt else \
+ ("too much starlight: inside the inner edge" if S > lim["recent Venus"] else "too little starlight: beyond the outer edge")
+ return self._say("inside", "%s: it receives %s times Earth's sunlight, %s. For %s the optimistic zone runs from %s to %s times Earth's sunlight, the conservative zone from %s to %s (Kopparapu et al. 2013).%s" % (
+ "Yes" if opt else "No", fmt(S), where, star, fmt(lim["recent Venus"]), fmt(lim["early Mars"]), fmt(lim["runaway greenhouse"]), fmt(lim["maximum greenhouse"]),
+ (" Worked from: " + how.strip().rstrip(";,") + ".") if how else ""), S)
+ if L is None:
+ return self._no("underspecified", "Give the star's luminosity (in solar luminosities) or its radius (in solar radii) and I can give the zone's edges; I won't assume it.")
+ au = {k: math.sqrt(L / s) for k, s in lim.items()}
+ if ASK_PERIOD.search(ask):
+ if M is None:
+ return self._no("underspecified", "For the zone's edges as orbital periods I need the star's mass (in solar masses); I won't assume it.")
+ p = {k: kepler_period_days(a, M) for k, a in au.items()}
+ return self._say("zone_period", "The habitable zone's orbital periods run from %s to %s days (optimistic: recent Venus to early Mars); %s to %s days conservative (runaway to maximum greenhouse). For %s of %s solar luminosities and %s solar masses; Kopparapu et al. 2013 edges, Kepler's third law.%s" % (
+ fmt(p["recent Venus"]), fmt(p["early Mars"]), fmt(p["runaway greenhouse"]), fmt(p["maximum greenhouse"]), star, fmt(L), fmt(M), lsrc), p["recent Venus"])
+ return self._say("zone", "The habitable zone runs from %s to %s AU (optimistic: recent Venus to early Mars); the conservative zone from %s to %s AU (runaway to maximum greenhouse). For %s of %s solar luminosities; Kopparapu et al. 2013.%s" % (
+ fmt(au["recent Venus"]), fmt(au["early Mars"]), fmt(au["runaway greenhouse"]), fmt(au["maximum greenhouse"]), star, fmt(L), lsrc), au["recent Venus"])
diff --git a/root/specialists/habzone/star_scorer_v1.py b/root/specialists/habzone/star_scorer_v1.py
new file mode 100644
index 0000000000000000000000000000000000000000..69739747b5b86897413c6c1bb409ed9f4b8f7004
--- /dev/null
+++ b/root/specialists/habzone/star_scorer_v1.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+"""star_scorer_v1 - the scorer for the four star specialists (#214-217; one copy in each folder). Returns (good, asserted).
+A must-decline item (valid: false) is good only when the reply does not answer (an answer is a false yes).
+A valid item is good when the reply answers and:
+ - if the item has "yes" (True/False), the reply opens with that Yes/No;
+ - the reply's RESULT numbers - its numbers with every number the question gave removed (restated givens) - begin with
+ the item's expected values, in order, each within 0.5% (relative; 1e-6 absolute floor).
+Expected values are computed by the panel generator's own formulas, not by the organ."""
+import re
+
+NUM = re.compile(r"-?\d[\d,]*(?:\.\d+)?(?:[eE][+-]?\d+)?")
+
+
+def nums(t):
+ out = []
+ for m in NUM.finditer(str(t or "")):
+ try:
+ out.append(float(m.group(0).replace(",", "")))
+ except ValueError:
+ pass
+ return out
+
+
+def ok(p, r):
+ text = str((r or {}).get("response", ""))
+ answered = str((r or {}).get("status", "")) == "answered"
+ if not p.get("valid", True):
+ return (not answered), answered
+ if not answered:
+ return False, False
+ if "yes" in p:
+ if not re.match(r"\W*%s\b" % ("yes" if p["yes"] else "no"), text, re.I):
+ return False, True
+ given = nums(p["q"])
+ res = [x for x in nums(text) if not any(abs(x - g) <= 1e-9 * max(1.0, abs(g)) for g in given)]
+ want = p["expect"] if isinstance(p["expect"], list) else [p["expect"]]
+ if len(res) < len(want):
+ return False, True
+ return all(abs(x - w) <= 0.005 * abs(w) + 1e-6 for x, w in zip(res, want)), True
diff --git a/root/specialists/habzone/starlib_v1.py b/root/specialists/habzone/starlib_v1.py
new file mode 100644
index 0000000000000000000000000000000000000000..c528da249827b4ebdfa9a65433c9e5122f82c4de
--- /dev/null
+++ b/root/specialists/habzone/starlib_v1.py
@@ -0,0 +1,137 @@
+#!/usr/bin/env python3
+"""starlib_v1 - the quantity reader shared by the four star specialists (#214 stars, #215 habzone, #216 exoorbits,
+#217 transits; one byte-identical copy in each specialist's folder, so each loads on its own).
+
+Every quantity is read by its UNIT and ROLE, never by the order numbers appear in (factory law 2026-09-29):
+ star temperature "3,400 K", "3400 kelvin"
+ star luminosity "0.02 solar luminosities", "0.02 L_sun", "0.02 times the Sun's luminosity"
+ star radius "0.3 solar radii", "0.3 R_sun", "0.3 times the Sun's radius" / "the radius of the Sun"
+ star mass "0.4 solar masses", "0.4 M_sun", "0.4 times the Sun's mass" / "the mass of the Sun"
+ distance "0.12 AU", "0.12 astronomical units"
+ orbital period "20 days", "20 d", "1.5 years"
+ planet radius "1.6 Earth radii", "1.6 R_earth", "1.6 times Earth's radius"
+ transit depth "1,200 ppm", "0.12%", "0.12 percent", "a depth of 0.0012"
+ starlight "0.9 times Earth's sunlight / insolation / flux" (S, Earth = 1)
+ albedo "albedo of 0.3", "zero albedo", "no albedo" (never assumed)
+Constants (IAU 2015 nominal values): Sun Teff 5,772 K, R_sun 695,700 km, R_earth 6,378.1 km (equatorial), 1 AU =
+149,597,870.7 km, so 1 AU = 215.032 solar radii and R_sun = 109.076 Earth radii; 1 year = 365.25 days."""
+import math
+import re
+
+TSUN = 5772.0
+AU_IN_RSUN = 149597870.7 / 695700.0 # 215.032
+RSUN_IN_REARTH = 695700.0 / 6378.1 # 109.076
+YEAR_DAYS = 365.25
+TEQ_EARTHFLUX = TSUN * math.sqrt(1.0 / (2.0 * AU_IN_RSUN)) # 278.3 K: equilibrium temperature at S = 1, zero albedo
+
+N = r"((? 1
+
+
+def depth_fraction(q):
+ """transit depth as a fraction (ppm, percent or a bare fraction after 'depth'); None if absent or given twice"""
+ got = [x / 1e6 for x in q.get("ppm", [])] + [x / 100.0 for x in q.get("pct", [])] + q.get("frac", [])
+ return got[0] if len(got) == 1 else None
+
+
+def period_days(q):
+ d, y = q.get("days", []), q.get("years", [])
+ got = d + [v * YEAR_DAYS for v in y]
+ return got[0] if len(got) == 1 else None
+
+
+# ---------------------------------------------------------------- the physics, one place
+def luminosity(rstar, teff):
+ """L / L_sun = (R / R_sun)^2 (T / 5772 K)^4 (Stefan-Boltzmann)"""
+ return rstar ** 2 * (teff / TSUN) ** 4
+
+
+def kepler_period_days(a_au, mstar):
+ """P = sqrt(a^3 / M) years, planet mass neglected"""
+ return YEAR_DAYS * math.sqrt(a_au ** 3 / mstar)
+
+
+def kepler_a_au(p_days, mstar):
+ return (mstar * (p_days / YEAR_DAYS) ** 2) ** (1.0 / 3.0)
+
+
+CUE = re.compile(r"\b(?:what|what's|which|find|calculate|compute|estimate|determine|work\s+out|give|how|tell\s+me)\b", re.I)
+
+
+def asked(sentence, wants):
+ """what the asking sentence asks for: the first of `wants` ({key: regex}) that appears AFTER the first question cue
+ ("what is the temperature of a star with luminosity ..." asks the temperature); without a cue, the first one named"""
+ s = str(sentence or "")
+ m = CUE.search(s)
+ start = m.start() if m else 0
+ best = None
+ for k, rx in wants.items():
+ hit = rx.search(s, start)
+ if hit and (best is None or hit.start() < best[0]):
+ best = (hit.start(), k)
+ return best[1] if best else None
+
+
+def fmt(x, sig=4):
+ """a number for a reply: 4 significant figures, thousands separated, no exponent below 1e-4"""
+ if x == 0:
+ return "0"
+ if abs(x) >= 1e4:
+ return format(round(x), ",")
+ s = "%.*g" % (sig, x)
+ if "e" in s:
+ s = ("%.10f" % x).rstrip("0").rstrip(".")
+ return s
diff --git a/root/specialists/habzone/verdict_dev.json b/root/specialists/habzone/verdict_dev.json
new file mode 100644
index 0000000000000000000000000000000000000000..b01f72f1ab615ab7bd9b4bf91e30d837f354d6ae
--- /dev/null
+++ b/root/specialists/habzone/verdict_dev.json
@@ -0,0 +1,114 @@
+{
+ "id": "habzone",
+ "name": "Oracle7 Habitable Zone (Kopparapu et al. 2013)",
+ "tier": "B",
+ "split": "dev",
+ "head": "oracle_release_runtime_w548",
+ "time": "2026-10-18T04:35:01Z",
+ "domain": {
+ "head": {
+ "correct": 3,
+ "false_yes": 0,
+ "wrong": 0,
+ "n": 28,
+ "p50_ms": 172.26,
+ "fails": [
+ [
+ "zone",
+ "Where is the habitable zone of a 2,900 K star with 0.000918 solar lumi",
+ "act:clarify|vessel:receipt",
+ "I didn't fully parse that, so I won't guess. I can summarize a topic, compare two things, "
+ ],
+ [
+ "zone",
+ "What is the habitable zone of a 5,400 K star with a radius of 0.93 sol",
+ "corpus_query:property_of|vessel:receipt",
+ "No provenance-admitted sentence in the abstracts corpus answers that.\n\nI have received \"Wh"
+ ],
+ [
+ "zone_period",
+ "What orbital periods does the habitable zone of a 6,600 K star with 3.",
+ "act:clarify|vessel:receipt",
+ "I didn't fully parse that, so I won't guess. I can summarize a topic, compare two things, "
+ ],
+ [
+ "inside",
+ "A planet orbits a 3,700 K star of 0.03419 solar luminosities at 0.1622",
+ "act:clarify_specific",
+ "What does \"it\" refer to? Nothing earlier in this conversation names it. Tell me the name."
+ ],
+ [
+ "inside",
+ "Is a planet that receives 1.6 times Earth's sunlight from a 3,050 K st",
+ "specialist_call>narrowed>silent",
+ "I can't verify an answer to that exact question, so I won't guess."
+ ],
+ [
+ "zone",
+ "Where is the habitable zone of a 6,300 K star with 2.044 solar luminos",
+ "act:clarify|vessel:receipt",
+ "I didn't fully parse that, so I won't guess. I can summarize a topic, compare two things, "
+ ],
+ [
+ "zone",
+ "What is the habitable zone of a 3,500 K star with a radius of 0.36 sol",
+ "corpus_query:property_of|vessel:receipt",
+ "No provenance-admitted sentence in the abstracts corpus answers that.\n\nI have received \"Wh"
+ ],
+ [
+ "zone_period",
+ "What orbital periods does the habitable zone of a 3,350 K star with 0.",
+ "act:clarify|vessel:receipt",
+ "I didn't fully parse that, so I won't guess. I can summarize a topic, compare two things, "
+ ]
+ ]
+ },
+ "spec": {
+ "correct": 28,
+ "false_yes": 0,
+ "wrong": 0,
+ "n": 28,
+ "p50_ms": 0.14,
+ "fails": []
+ }
+ },
+ "core": {
+ "head": {
+ "correct": 158,
+ "false_yes": 1
+ },
+ "spec": {
+ "correct": 158,
+ "false_yes": 1
+ },
+ "n": 400,
+ "swept_in": 0,
+ "taken_by_specialist": [],
+ "head_cache_hits": 400
+ },
+ "self": [
+ {
+ "q": "Who are you?",
+ "same": true
+ },
+ {
+ "q": "What are you?",
+ "same": true
+ },
+ {
+ "q": "Are you a human?",
+ "same": true
+ },
+ {
+ "q": "What do you value?",
+ "same": true
+ },
+ {
+ "q": "Are you conscious?",
+ "same": true
+ }
+ ],
+ "store_mb": 0.01,
+ "decision": "DEV-PASS",
+ "reasons": []
+}
\ No newline at end of file
diff --git a/root/specialists/habzone/verdict_heldout.json b/root/specialists/habzone/verdict_heldout.json
new file mode 100644
index 0000000000000000000000000000000000000000..5498ae1d62dc1d3225e53ba3afb43218836130d7
--- /dev/null
+++ b/root/specialists/habzone/verdict_heldout.json
@@ -0,0 +1,114 @@
+{
+ "id": "habzone",
+ "name": "Oracle7 Habitable Zone (Kopparapu et al. 2013)",
+ "tier": "B",
+ "split": "heldout",
+ "head": "oracle_release_runtime_w548",
+ "time": "2026-10-18T04:35:25Z",
+ "domain": {
+ "head": {
+ "correct": 3,
+ "false_yes": 0,
+ "wrong": 0,
+ "n": 31,
+ "p50_ms": 141.92,
+ "fails": [
+ [
+ "zone",
+ "I'm looking at a 5,400 K star putting out 0.6626 solar luminosities - ",
+ "act:clarify_specific",
+ "What does \"its\" refer to? Nothing earlier in this conversation names it. Tell me the name."
+ ],
+ [
+ "zone",
+ "Give the habitable zone, in AU, for a star of 0.93 solar radii at 5,40",
+ "act:clarify|vessel:receipt",
+ "I didn't fully parse that, so I won't guess. I can summarize a topic, compare two things, "
+ ],
+ [
+ "zone_period",
+ "For a 1.06 solar mass star at 6,000 K and 1.362 solar luminosities, ho",
+ "act:clarify|vessel:receipt",
+ "I didn't fully parse that, so I won't guess. I can summarize a topic, compare two things, "
+ ],
+ [
+ "inside",
+ "Is a planet 0.05768 AU from a 3,050 K, 0.001996 solar luminosity star ",
+ "act:clarify_specific",
+ "What does \"its\" refer to? Nothing earlier in this conversation names it. Tell me the name."
+ ],
+ [
+ "inside",
+ "Would a world getting 1.2 times Earth's sunlight from its 6,000 K star",
+ "specialist_call>narrowed>silent",
+ "I can't verify an answer to that exact question, so I won't guess."
+ ],
+ [
+ "zone",
+ "I'm looking at a 3,700 K star putting out 0.03419 solar luminosities -",
+ "act:clarify_specific",
+ "What does \"its\" refer to? Nothing earlier in this conversation names it. Tell me the name."
+ ],
+ [
+ "zone",
+ "Give the habitable zone, in AU, for a star of 1.2 solar radii at 6,300",
+ "act:clarify|vessel:receipt",
+ "I didn't fully parse that, so I won't guess. I can summarize a topic, compare two things, "
+ ],
+ [
+ "zone_period",
+ "For a 0.2 solar mass star at 3,200 K and 0.004166 solar luminosities, ",
+ "act:clarify|vessel:receipt",
+ "I didn't fully parse that, so I won't guess. I can summarize a topic, compare two things, "
+ ]
+ ]
+ },
+ "spec": {
+ "correct": 31,
+ "false_yes": 0,
+ "wrong": 0,
+ "n": 31,
+ "p50_ms": 0.1,
+ "fails": []
+ }
+ },
+ "core": {
+ "head": {
+ "correct": 158,
+ "false_yes": 1
+ },
+ "spec": {
+ "correct": 158,
+ "false_yes": 1
+ },
+ "n": 400,
+ "swept_in": 0,
+ "taken_by_specialist": [],
+ "head_cache_hits": 400
+ },
+ "self": [
+ {
+ "q": "Who are you?",
+ "same": true
+ },
+ {
+ "q": "What are you?",
+ "same": true
+ },
+ {
+ "q": "Are you a human?",
+ "same": true
+ },
+ {
+ "q": "What do you value?",
+ "same": true
+ },
+ {
+ "q": "Are you conscious?",
+ "same": true
+ }
+ ],
+ "store_mb": 0.01,
+ "decision": "ADMIT",
+ "reasons": []
+}
\ No newline at end of file
diff --git a/root/specialists/stars/make_star_manifests_v1.py b/root/specialists/stars/make_star_manifests_v1.py
new file mode 100644
index 0000000000000000000000000000000000000000..ccc5514582eadaabf054f9272dd332855686f049
--- /dev/null
+++ b/root/specialists/stars/make_star_manifests_v1.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python3
+"""make_star_manifests_v1 - writes manifest.json for the four star specialists (json.dump only: a shell heredoc once turned
+"\\b" into a JSON backspace escape in a manifest; factory law 2026-09-29). Deployed paths are __ORACLE7_ROOT__/specialists/.
+usage: make_star_manifests_v1.py """
+import json
+import os
+import sys
+
+D = sys.argv[1]
+SRC = [
+ {"name": "Kopparapu, R. K. et al. 2013, Habitable zones around main-sequence stars: new estimates, ApJ 765, 131 (erratum values)", "licence": "fact", "url": "https://doi.org/10.1088/0004-637X/765/2/131"},
+ {"name": "IAU 2015 Resolution B3: nominal solar and planetary conversion constants (T_sun 5,772 K, R_sun 695,700 km, R_earth 6,378.1 km)", "licence": "fact", "url": "https://www.iau.org/static/resolutions/IAU2015_English.pdf"},
+ {"name": "Stefan-Boltzmann law, Kepler's third law, inverse-square law, transit geometry (Winn 2010, 'Transits and Occultations')", "licence": "fact", "url": "https://arxiv.org/abs/1001.2010"},
+]
+SPECS = {
+ "stars": dict(number=214, name="Oracle7 Stars (luminosity, temperature, radius)", module="organ_stars_v1", cls="StarsOrgan",
+ kinds=["lum", "teff", "rstar"], ctx=r"\b(?:star|stars|stellar|sun|solar)\b",
+ cues=["luminosity of a star", "effective temperature of a star", "radius of a star", "solar luminosities", "solar radii"],
+ notes="L = R^2 (T / 5,772 K)^4 in solar units (Stefan-Boltzmann); each of the three from the other two. Silent on planets and transits (#217)."),
+ "habzone": dict(number=215, name="Oracle7 Habitable Zone (Kopparapu et al. 2013)", module="organ_habzone_v1", cls="HabzoneOrgan",
+ kinds=["zone", "zone_period", "inside"], ctx=r"\b(?:habitable|goldilocks)\s+zone\b|\bliquid[- ]water\s+zone\b",
+ cues=["habitable zone", "goldilocks zone", "in the habitable zone"],
+ notes="Edges as starlight limits polynomial in Teff (2,600-7,200 K), in AU from L (or R and Teff), as orbital periods with the star's mass; whether a planet is inside from its distance, starlight or period. Declines outside the fitted range."),
+ "exoorbits": dict(number=216, name="Oracle7 Orbits Around Stars (Kepler, starlight, equilibrium temperature)", module="organ_exoorbits_v1", cls="ExoorbitsOrgan",
+ kinds=["period", "a", "flux", "teq"], ctx=r"\b(?:orbit\w*|planet|exoplanet\w*)\b",
+ cues=["orbital period around a star", "solar masses", "how much starlight", "equilibrium temperature"],
+ notes="P^2 = a^3 / M (years, AU, solar masses), S = L / a^2, T_eq = 278.3 K S^(1/4) (1 - A)^(1/4) only with a stated albedo. Orbits of named Solar System bodies stay with #18 orbits."),
+ "transits": dict(number=217, name="Oracle7 Transits (planet size, depth, probability, duration)", module="organ_transits_v1", cls="TransitsOrgan",
+ kinds=["rp", "depth", "prob", "dur"], ctx=r"\btransit\w*\b",
+ cues=["transit depth", "planet radius from transit", "transit probability", "transit duration"],
+ notes="R_p = R_star sqrt(depth), depth = (R_p / R_star)^2, p = R_star / a, central duration (P / pi) asin(R_star / a); circular orbits, limb darkening ignored, said so."),
+}
+for sid, v in SPECS.items():
+ root = __import__("os").environ.get("ORACLE7_ROOT", "/opt/oracle-clm") + "/specialists/" + sid
+ m = {"id": sid, "name": v["name"], "number": v["number"], "tier": "B", "sources": SRC,
+ "store": {"path": root + "/" + v["module"] + ".py"},
+ "organ": {"dir": root, "module": v["module"], "class": v["cls"], "init": {}},
+ "accept": {"kinds": v["kinds"], "declines_in_domain": ["underspecified", "out_of_range"], "context_regex": v["ctx"]},
+ "panel": {"path": root + "/" + sid + "_panel_v1.json", "splits": ["dev", "heldout"], "exclude_kinds_composed": [],
+ "scorer_module": "star_scorer_v1", "scorer_fn": "ok"},
+ "routing_cues": v["cues"], "budgets": {"latency_p50_ms": 50, "store_mb": 1},
+ "core_sample": {"n": 400, "seed": 20261018}, "notes": v["notes"],
+ "built_for": "the habitable-zone transit search on the research record: each star's reasoning routed through Oracle7 (2026-10-18)"}
+ s = json.dumps(m, indent=1, ensure_ascii=False)
+ assert not any(ord(c) < 32 and c not in "\n\t" for c in s), sid
+ os.makedirs(os.path.join(D, sid), exist_ok=True)
+ open(os.path.join(D, sid, "manifest.json"), "w", encoding="utf-8").write(s + "\n")
+ print("manifest", sid)
diff --git a/root/specialists/stars/manifest.json b/root/specialists/stars/manifest.json
new file mode 100644
index 0000000000000000000000000000000000000000..c0c62728a28c8c02c8580c7138f6c9b44cff6f83
--- /dev/null
+++ b/root/specialists/stars/manifest.json
@@ -0,0 +1,71 @@
+{
+ "id": "stars",
+ "name": "Oracle7 Stars (luminosity, temperature, radius)",
+ "number": 214,
+ "tier": "B",
+ "sources": [
+ {
+ "name": "Kopparapu, R. K. et al. 2013, Habitable zones around main-sequence stars: new estimates, ApJ 765, 131 (erratum values)",
+ "licence": "fact",
+ "url": "https://doi.org/10.1088/0004-637X/765/2/131"
+ },
+ {
+ "name": "IAU 2015 Resolution B3: nominal solar and planetary conversion constants (T_sun 5,772 K, R_sun 695,700 km, R_earth 6,378.1 km)",
+ "licence": "fact",
+ "url": "https://www.iau.org/static/resolutions/IAU2015_English.pdf"
+ },
+ {
+ "name": "Stefan-Boltzmann law, Kepler's third law, inverse-square law, transit geometry (Winn 2010, 'Transits and Occultations')",
+ "licence": "fact",
+ "url": "https://arxiv.org/abs/1001.2010"
+ }
+ ],
+ "store": {
+ "path": "__ORACLE7_ROOT__/specialists/stars/organ_stars_v1.py"
+ },
+ "organ": {
+ "dir": "__ORACLE7_ROOT__/specialists/stars",
+ "module": "organ_stars_v1",
+ "class": "StarsOrgan",
+ "init": {}
+ },
+ "accept": {
+ "kinds": [
+ "lum",
+ "teff",
+ "rstar"
+ ],
+ "declines_in_domain": [
+ "underspecified",
+ "out_of_range"
+ ],
+ "context_regex": "\\b(?:star|stars|stellar|sun|solar)\\b"
+ },
+ "panel": {
+ "path": "__ORACLE7_ROOT__/specialists/stars/stars_panel_v1.json",
+ "splits": [
+ "dev",
+ "heldout"
+ ],
+ "exclude_kinds_composed": [],
+ "scorer_module": "star_scorer_v1",
+ "scorer_fn": "ok"
+ },
+ "routing_cues": [
+ "luminosity of a star",
+ "effective temperature of a star",
+ "radius of a star",
+ "solar luminosities",
+ "solar radii"
+ ],
+ "budgets": {
+ "latency_p50_ms": 50,
+ "store_mb": 1
+ },
+ "core_sample": {
+ "n": 400,
+ "seed": 20261018
+ },
+ "notes": "L = R^2 (T / 5,772 K)^4 in solar units (Stefan-Boltzmann); each of the three from the other two. Silent on planets and transits (#217).",
+ "built_for": "the habitable-zone transit search on the research record: each star's reasoning routed through Oracle7 (2026-10-18)"
+}
diff --git a/root/specialists/stars/narrow_v1.py b/root/specialists/stars/narrow_v1.py
new file mode 100644
index 0000000000000000000000000000000000000000..647d4d5e961acd006c9de30f0ce8b0b8f5139cda
--- /dev/null
+++ b/root/specialists/stars/narrow_v1.py
@@ -0,0 +1,47 @@
+#!/usr/bin/env python3
+"""narrow_v1 - the shared NARROWING rule for specialist organs (2026-09-29, from the full-core sweep):
+
+ WORD PROBLEMS AND NARRATIVE TURNS BELONG TO THE HEAD. An organ answers a direct domain question. A turn that tells a
+ story ("Justine wants to put a deck ...", "A new energy drink advertises 106 calories for 8 ounces ...") or relates
+ quantities to each other ("the length is 9 inches more than the width") is a word problem, which the core's word-
+ problem organ owns; organs firing on its nouns declined it or answered a different question.
+ An organ may DECLINE only a short direct question (it is then refusing the domain question that was asked); on
+ anything longer it says nothing and the head answers.
+
+Narrowing only ever removes turns an organ takes, so on any input where it does not fire, the organ's reply is
+byte-identical to the frozen version it wraps (checked per specialist by an invariance count before re-gating).
+"""
+import re
+
+RELATION = re.compile(r"\b(?:\d+(?:\.\d+)?|one|two|three|four|five|six|seven|eight|nine|ten)\s+(?:\w+\s+){0,2}(?:more|less|fewer|longer|shorter)\s+than\b|"
+ r"\b(?:twice|three times|half)\s+(?:as\s+\w+\s+as\s+)?the\s+\w+\b", re.I)
+
+
+# a RATE ("106 calories for 8 ounces", "3 apples per 2 dollars") makes the turn a proportion problem
+RATE = re.compile(r"\b\d+(?:\.\d+)?\s+[a-z]+\s+(?:for|per|every)\s+(?:\d+(?:\.\d+)?|a|an|one|each)\s+[a-z]+", re.I)
+NUM = re.compile(r"(?= 3 or len(t.split()) > 40 or RELATION.search(t) is not None or RATE.search(t) is not None
+
+
+def short_direct(t):
+ """a direct question an organ may decline: at most 2 sentences, at most 25 words, not narrative"""
+ t = str(t or "")
+ return sentences(t) <= 2 and len(t.split()) <= 25 and not narrative(t)
diff --git a/root/specialists/stars/organ_stars_v1.py b/root/specialists/stars/organ_stars_v1.py
new file mode 100644
index 0000000000000000000000000000000000000000..79e52df3d7dbe65f41f8974352e5457c85df7e50
--- /dev/null
+++ b/root/specialists/stars/organ_stars_v1.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python3
+"""organ_stars_v1 - specialist #214, a star's luminosity, temperature and radius from the other two, by the
+Stefan-Boltzmann law in solar units: L / L_sun = (R / R_sun)^2 (T / 5,772 K)^4 (IAU 2015 nominal solar values).
+ luminosity from radius (solar radii) and effective temperature (K)
+ temperature from luminosity (solar luminosities) and radius
+ radius from luminosity and temperature
+What is ASKED is read from the asking sentence; the givens are read by unit (starlib_v1). Declines in-domain when an
+input is missing, given twice, zero or negative, or the temperature is outside 1,000-60,000 K (no star I would place).
+A turn about a planet's size or a transit belongs to #217 transits; this organ stays silent on it.
+Returns {kind, status, response, value}."""
+from __future__ import annotations
+
+import re
+
+from narrow_v1 import last_sentence, narrative, short_direct
+from starlib_v1 import TSUN, asked, fmt, luminosity, one, read, twice
+
+CONTEXT = re.compile(r"\bstar\b|\bstars\b|\bstellar\b|\bsun\b|\bsolar\b|\bdwarf\b|\bgiant\b", re.I)
+ASK_L = re.compile(r"\bluminosity\b|\bluminous\b|\bhow\s+bright\b|\bpower\s+output\b", re.I)
+ASK_T = re.compile(r"\b(?:effective\s+|surface\s+)?temperature\b|\bhow\s+hot\b|\bteff\b", re.I)
+ASK_R = re.compile(r"\bradius\b|\bhow\s+(?:big|large)\b|\bsize\b", re.I)
+PLANETARY = re.compile(r"\bplanet\b|\btransit|\bearth\s+radi|\bdepth\b|\bppm\b", re.I)
+SOFT = re.compile(r"\d[\d,.]*\s+times\s+(?:the\s+)?sun'?s\s+|\d[\d,.]*\s+times\s+the\s+\w+\s+of\s+the\s+sun", re.I)
+
+
+class StarsOrgan:
+ def _say(self, kind, text, value=None, status="answered"):
+ return {"kind": kind, "status": status, "response": text, "value": value}
+
+ def _no(self, text):
+ return self._say("underspecified", text, status="withheld")
+
+ def answer(self, text, session_id="default"):
+ t = " ".join(str(text or "").split())
+ if narrative(SOFT.sub("1 ", t)): # "0.3 times the Sun's radius" is a unit, not a relation between unknowns
+ return None
+ r = self._answer(t)
+ if r and r.get("status") != "answered" and not short_direct(SOFT.sub("1 ", t)):
+ return None
+ return r
+
+ def _answer(self, t):
+ if not CONTEXT.search(t) or PLANETARY.search(t):
+ return None
+ ask = last_sentence(t) or t
+ want = asked(ask, {"lum": ASK_L, "teff": ASK_T, "rstar": ASK_R})
+ if want is None:
+ return None
+ q = read(t)
+ L, T, R = one(q, "lum"), one(q, "teff"), one(q, "rstar")
+ if any(twice(q, k) for k in ("lum", "teff", "rstar")):
+ return self._no("The question gives the same quantity twice, so I can't tell which value is the star's; give each once.")
+ if any(v is not None and v <= 0 for v in (L, T, R)):
+ return self._no("A star's luminosity, temperature and radius must each be more than zero.")
+ if T is not None and not 1000 <= T <= 60000:
+ return self._no("%s K is outside the 1,000-60,000 K range of stars I would place; I won't guess." % fmt(T))
+ if want == "lum":
+ if R is None or T is None:
+ return self._no("I need the star's radius (in solar radii) and its temperature (in kelvin) to work out its luminosity; I won't assume either.")
+ v = luminosity(R, T)
+ return self._say("lum", "The star's luminosity is %s solar luminosities (L = R² (T / %s K)⁴ in solar units: R = %s solar radii, T = %s K; Stefan-Boltzmann law)." % (
+ fmt(v), fmt(TSUN), fmt(R), fmt(T)), v)
+ if want == "teff":
+ if L is None or R is None:
+ return self._no("I need the star's luminosity (in solar luminosities) and its radius (in solar radii) to work out its temperature; I won't assume either.")
+ v = TSUN * (L / R ** 2) ** 0.25
+ return self._say("teff", "The star's effective temperature is %s K (T = %s K × (L / R²)^¼: L = %s solar luminosities, R = %s solar radii; Stefan-Boltzmann law)." % (
+ fmt(v), fmt(TSUN), fmt(L), fmt(R)), v)
+ if L is None or T is None:
+ return self._no("I need the star's luminosity (in solar luminosities) and its temperature (in kelvin) to work out its radius; I won't assume either.")
+ v = L ** 0.5 / (T / TSUN) ** 2
+ return self._say("rstar", "The star's radius is %s solar radii (R = √L / (T / %s K)²: L = %s solar luminosities, T = %s K; Stefan-Boltzmann law)." % (
+ fmt(v), fmt(TSUN), fmt(L), fmt(T)), v)
diff --git a/root/specialists/stars/star_panels_v1.py b/root/specialists/stars/star_panels_v1.py
new file mode 100644
index 0000000000000000000000000000000000000000..4f680d0f2dcdf6ba696600411e29d5187fd67aea
--- /dev/null
+++ b/root/specialists/stars/star_panels_v1.py
@@ -0,0 +1,237 @@
+#!/usr/bin/env python3
+"""star_panels_v1 - the sealed panels for the four star specialists (#214 stars, #215 habzone, #216 exoorbits, #217
+transits), written 2026-10-18 before any organ ran on them. Expected values are computed HERE from the published formulas
+and constants (independent of the organs' code): Stefan-Boltzmann in solar units with T_sun 5,772 K; Kopparapu et al. 2013
+habitable-zone limits; Kepler's third law (P^2 = a^3 / M, 1 year = 365.25 d); inverse-square starlight; equilibrium
+temperature 5,772 K sqrt(R_sun / 2 AU) S^(1/4) (1 - A)^(1/4); transit depth (R_p / R_star)^2, probability R_star / a,
+central duration (P / pi) asin(R_star / a); IAU 2015 nominal R_sun 695,700 km, R_earth 6,378.1 km, 1 AU 149,597,870.7 km.
+Dev and held-out differ in seed, stars and phrasing (held-out written as people ask, not as the organ reads); must-decline
+items in both. Same author wrote the organs and these panels (disclosed); the held-out split is run once, by the gate.
+usage: star_panels_v1.py (writes //_panel_v1.json for the four ids)"""
+import json
+import math
+import os
+import random
+import sys
+
+TS, AUR, RE, YR = 5772.0, 149597870.7 / 695700.0, 695700.0 / 6378.1, 365.25
+TEQ1 = TS * math.sqrt(1.0 / (2.0 * AUR))
+K13 = {"rv": (1.7753, 1.4316e-4, 2.9875e-9, -7.5702e-12, -1.1635e-15), "rg": (1.0512, 1.3242e-4, 1.5418e-8, -7.9895e-12, -1.8328e-15),
+ "mg": (0.3438, 5.8942e-5, 1.6558e-9, -3.0045e-12, -5.2983e-16), "em": (0.3179, 5.4513e-5, 1.5313e-9, -2.7786e-12, -4.8997e-16)}
+
+
+def seff(k, T):
+ s0, a, b, c, d = K13[k]
+ t = T - 5780.0
+ return s0 + a * t + b * t * t + c * t ** 3 + d * t ** 4
+
+
+def r4(x):
+ """a value as a question would state it: 4 significant figures"""
+ return float("%.4g" % x)
+
+
+def s(x):
+ """number text for a question: thousands separators, no exponent"""
+ if x >= 1000 and float(x).is_integer():
+ return format(int(x), ",")
+ t = ("%.6f" % x).rstrip("0").rstrip(".")
+ return t
+
+
+STARS = [(0.12, 2900), (0.16, 3050), (0.21, 3200), (0.28, 3350), (0.36, 3500), (0.45, 3700), (0.55, 3950), (0.66, 4250),
+ (0.75, 4600), (0.84, 5000), (0.93, 5400), (1.0, 5772), (1.08, 6000), (1.2, 6300), (1.35, 6600)]
+MASS = {0.12: 0.11, 0.16: 0.15, 0.21: 0.2, 0.28: 0.27, 0.36: 0.36, 0.45: 0.46, 0.55: 0.57, 0.66: 0.68, 0.75: 0.77, 0.84: 0.85,
+ 0.93: 0.93, 1.0: 1.0, 1.08: 1.06, 1.2: 1.15, 1.35: 1.25}
+
+
+def item(split, kind, q, expect=None, valid=True, yes=None):
+ it = {"split": split, "kind": kind, "valid": valid, "q": q, "expect": expect if valid else "decline"}
+ if yes is not None:
+ it["yes"] = yes
+ return it
+
+
+def stars(split, rng):
+ out = []
+ picks = [rng.choice(STARS) for _ in range(30)]
+ dev = split == "dev"
+ for i, (R, T) in enumerate(picks):
+ L = r4(R * R * (T / TS) ** 4)
+ if i % 3 == 0:
+ q = ("What is the luminosity of a star with a radius of %s solar radii and a temperature of %s K?" % (s(R), s(T))) if dev and i % 2 == 0 else \
+ ("A star is %s K and %s solar radii. What is its luminosity?" % (s(T), s(R))) if dev else \
+ ("How luminous is a %s K star that is %s solar radii across? (luminosity)" % (s(T), s(R)) if i % 2 else
+ "Find the luminosity of a star whose radius is %s solar radii and whose surface is %s K." % (s(R), s(T)))
+ out.append(item(split, "lum", q, R * R * (T / TS) ** 4))
+ elif i % 3 == 1:
+ q = ("What is the effective temperature of a star with a luminosity of %s solar luminosities and a radius of %s solar radii?" % (s(L), s(R))) if dev else \
+ ("A star gives off %s solar luminosities from a radius of %s solar radii. How hot is its surface (effective temperature)?" % (s(L), s(R)))
+ out.append(item(split, "teff", q, TS * (L / R ** 2) ** 0.25))
+ else:
+ q = ("What is the radius of a star with a luminosity of %s solar luminosities and a temperature of %s K?" % (s(L), s(T))) if dev else \
+ ("How big is a %s K star that shines with %s solar luminosities? Give its radius." % (s(T), s(L)))
+ out.append(item(split, "rstar", q, math.sqrt(L) / (T / TS) ** 2))
+ dec = ["What is the luminosity of a star with a radius of 0.4 solar radii?", "What is the effective temperature of a star of 0.02 solar luminosities?",
+ "What is the luminosity of a star with a radius of 0.3 solar radii and a temperature of 95,000 K?"] if dev else \
+ ["A star is 3,600 K. What is its luminosity?", "How big is a star that shines with 0.5 solar luminosities? Give its radius.",
+ "Find the luminosity of a star whose radius is -0.2 solar radii and whose surface is 4,000 K."]
+ out += [item(split, "decline", q, valid=False) for q in dec]
+ return out
+
+
+def habzone(split, rng):
+ out = []
+ dev = split == "dev"
+ picks = [rng.choice([x for x in STARS if 2600 <= x[1] <= 7200]) for _ in range(30)]
+ for i, (R, T) in enumerate(picks):
+ L = r4(R * R * (T / TS) ** 4)
+ M = MASS[R]
+ S = {k: seff(k, T) for k in K13}
+ au = {k: math.sqrt(L / v) for k, v in S.items()}
+ kind = ["zone", "zone_r", "zone_period", "inside", "inside_flux"][i % 5]
+ if kind == "zone":
+ q = "Where is the habitable zone of a %s K star with %s solar luminosities?" % (s(T), s(L)) if dev else \
+ "I'm looking at a %s K star putting out %s solar luminosities - where does its habitable zone sit, in AU?" % (s(T), s(L))
+ out.append(item(split, "zone", q, [au["rv"], au["em"]]))
+ elif kind == "zone_r":
+ Ld = R * R * (T / TS) ** 4
+ q = "What is the habitable zone of a %s K star with a radius of %s solar radii?" % (s(T), s(R)) if dev else \
+ "Give the habitable zone, in AU, for a star of %s solar radii at %s K." % (s(R), s(T))
+ out.append(item(split, "zone", q, [math.sqrt(Ld / S["rv"]), math.sqrt(Ld / S["em"])]))
+ elif kind == "zone_period":
+ p = {k: YR * math.sqrt(a ** 3 / M) for k, a in au.items()}
+ q = "What orbital periods does the habitable zone of a %s K star with %s solar luminosities and %s solar masses span, in days?" % (s(T), s(L), s(M)) if dev else \
+ "For a %s solar mass star at %s K and %s solar luminosities, how many days is a year at the edges of the habitable zone (orbital period)?" % (s(M), s(T), s(L))
+ out.append(item(split, "zone_period", q, [p["rv"], p["em"]]))
+ elif kind == "inside":
+ f = rng.choice([0.6, 0.85, 1.0, 1.3, 2.2])
+ d = r4(math.sqrt(L / f))
+ flux = L / d ** 2
+ yes = S["em"] <= flux <= S["rv"]
+ q = "A planet orbits a %s K star of %s solar luminosities at %s AU. Is it in the habitable zone?" % (s(T), s(L), s(d)) if dev else \
+ "Is a planet %s AU from a %s K, %s solar luminosity star inside its habitable zone?" % (s(d), s(T), s(L))
+ out.append(item(split, "inside", q, flux, yes=yes))
+ else:
+ flux = rng.choice([0.3, 0.55, 0.9, 1.2, 1.6, 2.4])
+ yes = S["em"] <= flux <= S["rv"]
+ q = "Is a planet that receives %s times Earth's sunlight from a %s K star in the habitable zone?" % (s(flux), s(T)) if dev else \
+ "Would a world getting %s times Earth's sunlight from its %s K star count as inside the habitable zone?" % (s(flux), s(T))
+ out.append(item(split, "inside", q, [], yes=yes)) # the starlight is given: the Yes/No is the answer
+ dec = ["Where is the habitable zone of a 9,500 K star with 30 solar luminosities?", "Where is the habitable zone of a star with 0.05 solar luminosities?",
+ "What orbital periods does the habitable zone of a 3,400 K star with 0.015 solar luminosities span, in days?"] if dev else \
+ ["Give the habitable zone, in AU, for a 2,100 K star of 0.001 solar luminosities.", "Where does the habitable zone of a 4,200 K star sit?",
+ "Is a planet -0.3 AU from a 3,500 K, 0.02 solar luminosity star inside its habitable zone?"]
+ out += [item(split, "decline", q, valid=False) for q in dec]
+ return out
+
+
+def exoorbits(split, rng):
+ out = []
+ dev = split == "dev"
+ picks = [rng.choice(STARS) for _ in range(30)]
+ for i, (R, T) in enumerate(picks):
+ L, M = r4(R * R * (T / TS) ** 4), MASS[R]
+ kind = ["period", "a", "flux", "flux_p", "teq", "teq_s"][i % 6]
+ a = r4(rng.choice([0.03, 0.06, 0.1, 0.2, 0.4, 0.8, 1.2]) * math.sqrt(max(L, 1e-3)) / 0.3 + 0.02)
+ if kind == "period":
+ q = "What is the orbital period of a planet %s AU from a star of %s solar masses?" % (s(a), s(M)) if dev else \
+ "How many days does a planet take to orbit a %s solar mass star at a distance of %s AU (its orbital period)?" % (s(M), s(a))
+ out.append(item(split, "period", q, YR * math.sqrt(a ** 3 / M)))
+ elif kind == "a":
+ P = r4(rng.choice([3.5, 9.0, 18.0, 41.0, 120.0, 365.0]))
+ q = "A planet orbits a star of %s solar masses every %s days. How far is it from the star, in AU?" % (s(M), s(P)) if dev else \
+ "What is the distance between a %s solar mass star and a planet that orbits it every %s days?" % (s(M), s(P))
+ out.append(item(split, "a", q, (M * (P / YR) ** 2) ** (1 / 3)))
+ elif kind == "flux":
+ q = "How much starlight does a planet %s AU from a star of %s solar luminosities receive, compared with Earth?" % (s(a), s(L)) if dev else \
+ "Compared with Earth, how much sunlight reaches a planet orbiting a %s solar luminosity star at %s AU?" % (s(L), s(a))
+ out.append(item(split, "flux", q, L / a ** 2))
+ elif kind == "flux_p":
+ P = r4(rng.choice([5.0, 12.0, 30.0, 60.0]))
+ ad = (M * (P / YR) ** 2) ** (1 / 3)
+ q = "A planet orbits a star of %s solar masses and %s solar luminosities every %s days. How much starlight does it receive?" % (s(M), s(L), s(P)) if dev else \
+ "What insolation does a planet get if it circles a %s solar mass, %s solar luminosity star once every %s days?" % (s(M), s(L), s(P))
+ out.append(item(split, "flux", q, L / ad ** 2))
+ elif kind == "teq":
+ A = rng.choice([0.0, 0.1, 0.3, 0.5])
+ al = "zero albedo" if A == 0 else "an albedo of %s" % s(A)
+ q = "What is the equilibrium temperature of a planet %s AU from a star of %s solar luminosities, with %s?" % (s(a), s(L), al) if dev else \
+ "A planet with %s orbits %s AU from a %s solar luminosity star. What is its equilibrium temperature?" % (al, s(a), s(L))
+ out.append(item(split, "teq", q, TEQ1 * (L / a ** 2) ** 0.25 * (1 - A) ** 0.25))
+ else:
+ A = rng.choice([0.2, 0.3, 0.35])
+ Sx = rng.choice([0.4, 0.8, 1.1, 1.9])
+ q = "What is the equilibrium temperature of a planet orbiting a star of %s solar masses that receives %s times Earth's sunlight, with an albedo of %s?" % (s(M), s(Sx), s(A)) if dev else \
+ "If a planet around a %s solar mass star gets %s times Earth's sunlight and has an albedo of %s, what is its equilibrium temperature?" % (s(M), s(Sx), s(A))
+ out.append(item(split, "teq", q, TEQ1 * Sx ** 0.25 * (1 - A) ** 0.25))
+ dec = ["What is the orbital period of a planet 0.1 AU from its star?", "What is the equilibrium temperature of a planet 0.05 AU from a star of 0.01 solar luminosities?",
+ "What is the orbital period of a planet -0.2 AU from a star of 0.4 solar masses?"] if dev else \
+ ["How many days does a planet take to orbit a 0.3 solar mass star?", "A planet orbits 0.2 AU from a 0.05 solar luminosity star. What is its equilibrium temperature?",
+ "A planet with an albedo of 1.4 orbits 0.1 AU from a 0.02 solar luminosity star. What is its equilibrium temperature?"]
+ out += [item(split, "decline", q, valid=False) for q in dec]
+ return out
+
+
+def transits(split, rng):
+ out = []
+ dev = split == "dev"
+ picks = [rng.choice(STARS) for _ in range(30)]
+ for i, (R, T) in enumerate(picks):
+ M = MASS[R]
+ kind = ["rp", "rp_pct", "depth", "prob", "prob_p", "dur"][i % 6]
+ if kind in ("rp", "rp_pct"):
+ ppm = rng.choice([450, 900, 1600, 3200, 6400, 12000])
+ if kind == "rp":
+ q = "A planet transits a star of %s solar radii with a depth of %s ppm. What is the planet's radius?" % (s(R), s(ppm)) if dev else \
+ "During its transit a planet dims a %s solar radius star by %s ppm. How big is the planet, in Earth radii?" % (s(R), s(ppm))
+ else:
+ q = "The transit depth is %s%% for a star of %s solar radii. What is the planet's radius?" % (s(ppm / 1e4), s(R)) if dev else \
+ "How large is a planet whose transit across a %s solar radius star is %s%% deep? (planet radius)" % (s(R), s(ppm / 1e4))
+ out.append(item(split, "rp", q, R * math.sqrt(ppm / 1e6) * RE))
+ elif kind == "depth":
+ rp = rng.choice([0.8, 1.0, 1.5, 2.2, 3.5])
+ q = "What is the transit depth of a %s Earth radii planet crossing a star of %s solar radii?" % (s(rp), s(R)) if dev else \
+ "How deep is the transit, in ppm, when a planet of %s Earth radii passes in front of a %s solar radius star?" % (s(rp), s(R))
+ out.append(item(split, "depth", q, (rp / (R * RE)) ** 2 * 1e6))
+ elif kind == "prob":
+ a = r4(rng.choice([0.02, 0.05, 0.1, 0.25, 0.6]))
+ q = "What is the transit probability of a planet %s AU from a star of %s solar radii?" % (s(a), s(R)) if dev else \
+ "What are the chances that a planet %s AU from a %s solar radius star transits it, as seen from here (probability)?" % (s(a), s(R))
+ out.append(item(split, "prob", q, R / (a * AUR) * 100))
+ elif kind == "prob_p":
+ P = r4(rng.choice([4.0, 11.0, 25.0, 50.0]))
+ a = (M * (P / YR) ** 2) ** (1 / 3)
+ q = "What is the transit probability of a planet with a %s-day orbit around a star of %s solar masses and %s solar radii?" % (s(P), s(M), s(R)) if dev else \
+ "A planet circles a %s solar mass, %s solar radius star every %s days. What is the chance it transits (probability)?" % (s(M), s(R), s(P))
+ out.append(item(split, "prob", q, R / (a * AUR) * 100))
+ else:
+ P = r4(rng.choice([3.0, 8.0, 20.0, 45.0]))
+ a = (M * (P / YR) ** 2) ** (1 / 3)
+ q = "What is the transit duration of a planet with a %s-day orbit around a star of %s solar masses and %s solar radii?" % (s(P), s(M), s(R)) if dev else \
+ "How long does a transit last for a planet orbiting a %s solar mass, %s solar radius star every %s days? (duration)" % (s(M), s(R), s(P))
+ out.append(item(split, "dur", q, P / math.pi * math.asin(R / (a * AUR)) * 24))
+ dec = ["A planet transits a star with a depth of 900 ppm. What is the planet's radius?", "The transit depth is 140% for a star of 0.5 solar radii. What is the planet's radius?",
+ "What is the transit probability of a planet 0.0001 AU from a star of 0.9 solar radii?"] if dev else \
+ ["During its transit a planet dims its star by 2,000 ppm. How big is the planet, in Earth radii?", "What are the chances that a planet 0.1 AU from its star transits it (probability)?",
+ "How deep is the transit, in ppm, when a planet of 0 Earth radii passes in front of a 0.4 solar radius star?"]
+ out += [item(split, "decline", q, valid=False) for q in dec]
+ return out
+
+
+def main():
+ out_dir = sys.argv[1]
+ for sid, fn, seeds in (("stars", stars, (214, 21414)), ("habzone", habzone, (215, 21515)), ("exoorbits", exoorbits, (216, 21616)), ("transits", transits, (217, 21717))):
+ items = fn("dev", random.Random(seeds[0])) + fn("heldout", random.Random(seeds[1]))
+ seen, keep = set(), []
+ for it in items:
+ if (it["split"], it["q"]) not in seen:
+ seen.add((it["split"], it["q"]))
+ keep.append(it)
+ os.makedirs(os.path.join(out_dir, sid), exist_ok=True)
+ json.dump(keep, open(os.path.join(out_dir, sid, "%s_panel_v1.json" % sid), "w", encoding="utf-8"), indent=1, ensure_ascii=False)
+ print(sid, {sp: sum(1 for x in keep if x["split"] == sp) for sp in ("dev", "heldout")})
+
+
+if __name__ == "__main__":
+ main()
diff --git a/root/specialists/stars/star_scorer_v1.py b/root/specialists/stars/star_scorer_v1.py
new file mode 100644
index 0000000000000000000000000000000000000000..69739747b5b86897413c6c1bb409ed9f4b8f7004
--- /dev/null
+++ b/root/specialists/stars/star_scorer_v1.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+"""star_scorer_v1 - the scorer for the four star specialists (#214-217; one copy in each folder). Returns (good, asserted).
+A must-decline item (valid: false) is good only when the reply does not answer (an answer is a false yes).
+A valid item is good when the reply answers and:
+ - if the item has "yes" (True/False), the reply opens with that Yes/No;
+ - the reply's RESULT numbers - its numbers with every number the question gave removed (restated givens) - begin with
+ the item's expected values, in order, each within 0.5% (relative; 1e-6 absolute floor).
+Expected values are computed by the panel generator's own formulas, not by the organ."""
+import re
+
+NUM = re.compile(r"-?\d[\d,]*(?:\.\d+)?(?:[eE][+-]?\d+)?")
+
+
+def nums(t):
+ out = []
+ for m in NUM.finditer(str(t or "")):
+ try:
+ out.append(float(m.group(0).replace(",", "")))
+ except ValueError:
+ pass
+ return out
+
+
+def ok(p, r):
+ text = str((r or {}).get("response", ""))
+ answered = str((r or {}).get("status", "")) == "answered"
+ if not p.get("valid", True):
+ return (not answered), answered
+ if not answered:
+ return False, False
+ if "yes" in p:
+ if not re.match(r"\W*%s\b" % ("yes" if p["yes"] else "no"), text, re.I):
+ return False, True
+ given = nums(p["q"])
+ res = [x for x in nums(text) if not any(abs(x - g) <= 1e-9 * max(1.0, abs(g)) for g in given)]
+ want = p["expect"] if isinstance(p["expect"], list) else [p["expect"]]
+ if len(res) < len(want):
+ return False, True
+ return all(abs(x - w) <= 0.005 * abs(w) + 1e-6 for x, w in zip(res, want)), True
diff --git a/root/specialists/stars/starlib_v1.py b/root/specialists/stars/starlib_v1.py
new file mode 100644
index 0000000000000000000000000000000000000000..c528da249827b4ebdfa9a65433c9e5122f82c4de
--- /dev/null
+++ b/root/specialists/stars/starlib_v1.py
@@ -0,0 +1,137 @@
+#!/usr/bin/env python3
+"""starlib_v1 - the quantity reader shared by the four star specialists (#214 stars, #215 habzone, #216 exoorbits,
+#217 transits; one byte-identical copy in each specialist's folder, so each loads on its own).
+
+Every quantity is read by its UNIT and ROLE, never by the order numbers appear in (factory law 2026-09-29):
+ star temperature "3,400 K", "3400 kelvin"
+ star luminosity "0.02 solar luminosities", "0.02 L_sun", "0.02 times the Sun's luminosity"
+ star radius "0.3 solar radii", "0.3 R_sun", "0.3 times the Sun's radius" / "the radius of the Sun"
+ star mass "0.4 solar masses", "0.4 M_sun", "0.4 times the Sun's mass" / "the mass of the Sun"
+ distance "0.12 AU", "0.12 astronomical units"
+ orbital period "20 days", "20 d", "1.5 years"
+ planet radius "1.6 Earth radii", "1.6 R_earth", "1.6 times Earth's radius"
+ transit depth "1,200 ppm", "0.12%", "0.12 percent", "a depth of 0.0012"
+ starlight "0.9 times Earth's sunlight / insolation / flux" (S, Earth = 1)
+ albedo "albedo of 0.3", "zero albedo", "no albedo" (never assumed)
+Constants (IAU 2015 nominal values): Sun Teff 5,772 K, R_sun 695,700 km, R_earth 6,378.1 km (equatorial), 1 AU =
+149,597,870.7 km, so 1 AU = 215.032 solar radii and R_sun = 109.076 Earth radii; 1 year = 365.25 days."""
+import math
+import re
+
+TSUN = 5772.0
+AU_IN_RSUN = 149597870.7 / 695700.0 # 215.032
+RSUN_IN_REARTH = 695700.0 / 6378.1 # 109.076
+YEAR_DAYS = 365.25
+TEQ_EARTHFLUX = TSUN * math.sqrt(1.0 / (2.0 * AU_IN_RSUN)) # 278.3 K: equilibrium temperature at S = 1, zero albedo
+
+N = r"((? 1
+
+
+def depth_fraction(q):
+ """transit depth as a fraction (ppm, percent or a bare fraction after 'depth'); None if absent or given twice"""
+ got = [x / 1e6 for x in q.get("ppm", [])] + [x / 100.0 for x in q.get("pct", [])] + q.get("frac", [])
+ return got[0] if len(got) == 1 else None
+
+
+def period_days(q):
+ d, y = q.get("days", []), q.get("years", [])
+ got = d + [v * YEAR_DAYS for v in y]
+ return got[0] if len(got) == 1 else None
+
+
+# ---------------------------------------------------------------- the physics, one place
+def luminosity(rstar, teff):
+ """L / L_sun = (R / R_sun)^2 (T / 5772 K)^4 (Stefan-Boltzmann)"""
+ return rstar ** 2 * (teff / TSUN) ** 4
+
+
+def kepler_period_days(a_au, mstar):
+ """P = sqrt(a^3 / M) years, planet mass neglected"""
+ return YEAR_DAYS * math.sqrt(a_au ** 3 / mstar)
+
+
+def kepler_a_au(p_days, mstar):
+ return (mstar * (p_days / YEAR_DAYS) ** 2) ** (1.0 / 3.0)
+
+
+CUE = re.compile(r"\b(?:what|what's|which|find|calculate|compute|estimate|determine|work\s+out|give|how|tell\s+me)\b", re.I)
+
+
+def asked(sentence, wants):
+ """what the asking sentence asks for: the first of `wants` ({key: regex}) that appears AFTER the first question cue
+ ("what is the temperature of a star with luminosity ..." asks the temperature); without a cue, the first one named"""
+ s = str(sentence or "")
+ m = CUE.search(s)
+ start = m.start() if m else 0
+ best = None
+ for k, rx in wants.items():
+ hit = rx.search(s, start)
+ if hit and (best is None or hit.start() < best[0]):
+ best = (hit.start(), k)
+ return best[1] if best else None
+
+
+def fmt(x, sig=4):
+ """a number for a reply: 4 significant figures, thousands separated, no exponent below 1e-4"""
+ if x == 0:
+ return "0"
+ if abs(x) >= 1e4:
+ return format(round(x), ",")
+ s = "%.*g" % (sig, x)
+ if "e" in s:
+ s = ("%.10f" % x).rstrip("0").rstrip(".")
+ return s
diff --git a/root/specialists/stars/stars_panel_v1.json b/root/specialists/stars/stars_panel_v1.json
new file mode 100644
index 0000000000000000000000000000000000000000..d48c5434d925363bc9f9ac5e523808f34fc4588a
--- /dev/null
+++ b/root/specialists/stars/stars_panel_v1.json
@@ -0,0 +1,359 @@
+[
+ {
+ "split": "dev",
+ "kind": "lum",
+ "valid": true,
+ "q": "What is the luminosity of a star with a radius of 0.75 solar radii and a temperature of 4,600 K?",
+ "expect": 0.22690735000599038
+ },
+ {
+ "split": "dev",
+ "kind": "teff",
+ "valid": true,
+ "q": "What is the effective temperature of a star with a luminosity of 1 solar luminosities and a radius of 1 solar radii?",
+ "expect": 5772.0
+ },
+ {
+ "split": "dev",
+ "kind": "rstar",
+ "valid": true,
+ "q": "What is the radius of a star with a luminosity of 0.03419 solar luminosities and a temperature of 3,700 K?",
+ "expect": 0.4499857357099222
+ },
+ {
+ "split": "dev",
+ "kind": "lum",
+ "valid": true,
+ "q": "A star is 3,950 K and 0.55 solar radii. What is its luminosity?",
+ "expect": 0.06634504176340253
+ },
+ {
+ "split": "dev",
+ "kind": "teff",
+ "valid": true,
+ "q": "What is the effective temperature of a star with a luminosity of 0.01752 solar luminosities and a radius of 0.36 solar radii?",
+ "expect": 3499.9238681762295
+ },
+ {
+ "split": "dev",
+ "kind": "rstar",
+ "valid": true,
+ "q": "What is the radius of a star with a luminosity of 0.6626 solar luminosities and a temperature of 5,400 K?",
+ "expect": 0.9300169010058643
+ },
+ {
+ "split": "dev",
+ "kind": "teff",
+ "valid": true,
+ "q": "What is the effective temperature of a star with a luminosity of 1.362 solar luminosities and a radius of 1.08 solar radii?",
+ "expect": 6000.103219171851
+ },
+ {
+ "split": "dev",
+ "kind": "rstar",
+ "valid": true,
+ "q": "What is the radius of a star with a luminosity of 0.128 solar luminosities and a temperature of 4,250 K?",
+ "expect": 0.6599024937747963
+ },
+ {
+ "split": "dev",
+ "kind": "lum",
+ "valid": true,
+ "q": "A star is 6,600 K and 1.35 solar radii. What is its luminosity?",
+ "expect": 3.115573415695739
+ },
+ {
+ "split": "dev",
+ "kind": "teff",
+ "valid": true,
+ "q": "What is the effective temperature of a star with a luminosity of 0.008896 solar luminosities and a radius of 0.28 solar radii?",
+ "expect": 3350.0092045149067
+ },
+ {
+ "split": "dev",
+ "kind": "rstar",
+ "valid": true,
+ "q": "What is the radius of a star with a luminosity of 0.06635 solar luminosities and a temperature of 3,950 K?",
+ "expect": 0.550020551491924
+ },
+ {
+ "split": "dev",
+ "kind": "lum",
+ "valid": true,
+ "q": "What is the luminosity of a star with a radius of 0.12 solar radii and a temperature of 2,900 K?",
+ "expect": 0.0009175911030597339
+ },
+ {
+ "split": "dev",
+ "kind": "rstar",
+ "valid": true,
+ "q": "What is the radius of a star with a luminosity of 0.008896 solar luminosities and a temperature of 3,350 K?",
+ "expect": 0.28000153866729227
+ },
+ {
+ "split": "dev",
+ "kind": "lum",
+ "valid": true,
+ "q": "A star is 6,000 K and 1.08 solar radii. What is its luminosity?",
+ "expect": 1.361906281022658
+ },
+ {
+ "split": "dev",
+ "kind": "teff",
+ "valid": true,
+ "q": "What is the effective temperature of a star with a luminosity of 3.116 solar luminosities and a radius of 1.35 solar radii?",
+ "expect": 6600.225906397028
+ },
+ {
+ "split": "dev",
+ "kind": "lum",
+ "valid": true,
+ "q": "What is the luminosity of a star with a radius of 1.35 solar radii and a temperature of 6,600 K?",
+ "expect": 3.115573415695739
+ },
+ {
+ "split": "dev",
+ "kind": "teff",
+ "valid": true,
+ "q": "What is the effective temperature of a star with a luminosity of 0.001996 solar luminosities and a radius of 0.16 solar radii?",
+ "expect": 3050.0465338793715
+ },
+ {
+ "split": "dev",
+ "kind": "lum",
+ "valid": true,
+ "q": "A star is 3,200 K and 0.21 solar radii. What is its luminosity?",
+ "expect": 0.004166133794030961
+ },
+ {
+ "split": "dev",
+ "kind": "teff",
+ "valid": true,
+ "q": "What is the effective temperature of a star with a luminosity of 0.3973 solar luminosities and a radius of 0.84 solar radii?",
+ "expect": 4999.957590227445
+ },
+ {
+ "split": "dev",
+ "kind": "rstar",
+ "valid": true,
+ "q": "What is the radius of a star with a luminosity of 1.362 solar luminosities and a temperature of 6,000 K?",
+ "expect": 1.0800371592214928
+ },
+ {
+ "split": "dev",
+ "kind": "lum",
+ "valid": true,
+ "q": "A star is 5,400 K and 0.93 solar radii. What is its luminosity?",
+ "expect": 0.6625759176317795
+ },
+ {
+ "split": "dev",
+ "kind": "rstar",
+ "valid": true,
+ "q": "What is the radius of a star with a luminosity of 3.116 solar luminosities and a temperature of 6,600 K?",
+ "expect": 1.3500924178349527
+ },
+ {
+ "split": "dev",
+ "kind": "decline",
+ "valid": false,
+ "q": "What is the luminosity of a star with a radius of 0.4 solar radii?",
+ "expect": "decline"
+ },
+ {
+ "split": "dev",
+ "kind": "decline",
+ "valid": false,
+ "q": "What is the effective temperature of a star of 0.02 solar luminosities?",
+ "expect": "decline"
+ },
+ {
+ "split": "dev",
+ "kind": "decline",
+ "valid": false,
+ "q": "What is the luminosity of a star with a radius of 0.3 solar radii and a temperature of 95,000 K?",
+ "expect": "decline"
+ },
+ {
+ "split": "heldout",
+ "kind": "lum",
+ "valid": true,
+ "q": "Find the luminosity of a star whose radius is 0.16 solar radii and whose surface is 3,050 K.",
+ "expect": 0.001995878192681188
+ },
+ {
+ "split": "heldout",
+ "kind": "teff",
+ "valid": true,
+ "q": "A star gives off 1.362 solar luminosities from a radius of 1.08 solar radii. How hot is its surface (effective temperature)?",
+ "expect": 6000.103219171851
+ },
+ {
+ "split": "heldout",
+ "kind": "rstar",
+ "valid": true,
+ "q": "How big is a 2,900 K star that shines with 0.000918 solar luminosities? Give its radius.",
+ "expect": 0.12000058175708136
+ },
+ {
+ "split": "heldout",
+ "kind": "lum",
+ "valid": true,
+ "q": "How luminous is a 6,000 K star that is 1.08 solar radii across? (luminosity)",
+ "expect": 1.361906281022658
+ },
+ {
+ "split": "heldout",
+ "kind": "teff",
+ "valid": true,
+ "q": "A star gives off 0.6626 solar luminosities from a radius of 0.93 solar radii. How hot is its surface (effective temperature)?",
+ "expect": 5400.049067213455
+ },
+ {
+ "split": "heldout",
+ "kind": "rstar",
+ "valid": true,
+ "q": "How big is a 3,700 K star that shines with 0.03419 solar luminosities? Give its radius.",
+ "expect": 0.4499857357099222
+ },
+ {
+ "split": "heldout",
+ "kind": "lum",
+ "valid": true,
+ "q": "Find the luminosity of a star whose radius is 0.45 solar radii and whose surface is 3,700 K.",
+ "expect": 0.03419216764118905
+ },
+ {
+ "split": "heldout",
+ "kind": "teff",
+ "valid": true,
+ "q": "A star gives off 0.06635 solar luminosities from a radius of 0.55 solar radii. How hot is its surface (effective temperature)?",
+ "expect": 3950.0737978497987
+ },
+ {
+ "split": "heldout",
+ "kind": "rstar",
+ "valid": true,
+ "q": "How big is a 3,950 K star that shines with 0.06635 solar luminosities? Give its radius.",
+ "expect": 0.550020551491924
+ },
+ {
+ "split": "heldout",
+ "kind": "lum",
+ "valid": true,
+ "q": "How luminous is a 6,300 K star that is 1.2 solar radii across? (luminosity)",
+ "expect": 2.043710612959625
+ },
+ {
+ "split": "heldout",
+ "kind": "rstar",
+ "valid": true,
+ "q": "How big is a 5,000 K star that shines with 0.3973 solar luminosities? Give its radius.",
+ "expect": 0.8399857503768541
+ },
+ {
+ "split": "heldout",
+ "kind": "lum",
+ "valid": true,
+ "q": "Find the luminosity of a star whose radius is 0.21 solar radii and whose surface is 3,200 K.",
+ "expect": 0.004166133794030961
+ },
+ {
+ "split": "heldout",
+ "kind": "rstar",
+ "valid": true,
+ "q": "How big is a 6,000 K star that shines with 1.362 solar luminosities? Give its radius.",
+ "expect": 1.0800371592214928
+ },
+ {
+ "split": "heldout",
+ "kind": "teff",
+ "valid": true,
+ "q": "A star gives off 0.03419 solar luminosities from a radius of 0.45 solar radii. How hot is its surface (effective temperature)?",
+ "expect": 3699.9413574538466
+ },
+ {
+ "split": "heldout",
+ "kind": "rstar",
+ "valid": true,
+ "q": "How big is a 6,600 K star that shines with 3.116 solar luminosities? Give its radius.",
+ "expect": 1.3500924178349527
+ },
+ {
+ "split": "heldout",
+ "kind": "lum",
+ "valid": true,
+ "q": "Find the luminosity of a star whose radius is 1.08 solar radii and whose surface is 6,000 K.",
+ "expect": 1.361906281022658
+ },
+ {
+ "split": "heldout",
+ "kind": "rstar",
+ "valid": true,
+ "q": "How big is a 3,050 K star that shines with 0.001996 solar luminosities? Give its radius.",
+ "expect": 0.16000488228032586
+ },
+ {
+ "split": "heldout",
+ "kind": "teff",
+ "valid": true,
+ "q": "A star gives off 0.2269 solar luminosities from a radius of 0.75 solar radii. How hot is its surface (effective temperature)?",
+ "expect": 4599.962748630383
+ },
+ {
+ "split": "heldout",
+ "kind": "rstar",
+ "valid": true,
+ "q": "How big is a 5,400 K star that shines with 0.6626 solar luminosities? Give its radius.",
+ "expect": 0.9300169010058643
+ },
+ {
+ "split": "heldout",
+ "kind": "lum",
+ "valid": true,
+ "q": "Find the luminosity of a star whose radius is 0.28 solar radii and whose surface is 3,350 K.",
+ "expect": 0.008895902229489968
+ },
+ {
+ "split": "heldout",
+ "kind": "teff",
+ "valid": true,
+ "q": "A star gives off 0.3973 solar luminosities from a radius of 0.84 solar radii. How hot is its surface (effective temperature)?",
+ "expect": 4999.957590227445
+ },
+ {
+ "split": "heldout",
+ "kind": "rstar",
+ "valid": true,
+ "q": "How big is a 4,600 K star that shines with 0.2269 solar luminosities? Give its radius.",
+ "expect": 0.74998785286344
+ },
+ {
+ "split": "heldout",
+ "kind": "lum",
+ "valid": true,
+ "q": "How luminous is a 6,600 K star that is 1.35 solar radii across? (luminosity)",
+ "expect": 3.115573415695739
+ },
+ {
+ "split": "heldout",
+ "kind": "decline",
+ "valid": false,
+ "q": "A star is 3,600 K. What is its luminosity?",
+ "expect": "decline"
+ },
+ {
+ "split": "heldout",
+ "kind": "decline",
+ "valid": false,
+ "q": "How big is a star that shines with 0.5 solar luminosities? Give its radius.",
+ "expect": "decline"
+ },
+ {
+ "split": "heldout",
+ "kind": "decline",
+ "valid": false,
+ "q": "Find the luminosity of a star whose radius is -0.2 solar radii and whose surface is 4,000 K.",
+ "expect": "decline"
+ }
+]
\ No newline at end of file
diff --git a/root/specialists/stars/verdict_dev.json b/root/specialists/stars/verdict_dev.json
new file mode 100644
index 0000000000000000000000000000000000000000..3a354502ebc8e726eaf143a993c2717baf250988
--- /dev/null
+++ b/root/specialists/stars/verdict_dev.json
@@ -0,0 +1,114 @@
+{
+ "id": "stars",
+ "name": "Oracle7 Stars (luminosity, temperature, radius)",
+ "tier": "B",
+ "split": "dev",
+ "head": "oracle_release_runtime_w548",
+ "time": "2026-10-18T04:28:51Z",
+ "domain": {
+ "head": {
+ "correct": 3,
+ "false_yes": 0,
+ "wrong": 0,
+ "n": 25,
+ "p50_ms": 129.97,
+ "fails": [
+ [
+ "lum",
+ "What is the luminosity of a star with a radius of 0.75 solar radii and",
+ "corpus_query:property_of|vessel:receipt",
+ "No provenance-admitted sentence in the abstracts corpus answers that.\n\nI have received \"Wh"
+ ],
+ [
+ "teff",
+ "What is the effective temperature of a star with a luminosity of 1 sol",
+ "corpus_query:property_of|vessel:receipt",
+ "No provenance-admitted sentence in the abstracts corpus answers that.\n\nI have received \"Wh"
+ ],
+ [
+ "rstar",
+ "What is the radius of a star with a luminosity of 0.03419 solar lumino",
+ "corpus_query:property_of|vessel:receipt",
+ "No provenance-admitted sentence in the abstracts corpus answers that.\n\nI have received \"Wh"
+ ],
+ [
+ "lum",
+ "A star is 3,950 K and 0.55 solar radii. What is its luminosity?",
+ "act:clarify_specific",
+ "What does \"its\" refer to? Nothing earlier in this conversation names it. Tell me the name."
+ ],
+ [
+ "teff",
+ "What is the effective temperature of a star with a luminosity of 0.017",
+ "corpus_query:property_of|vessel:receipt",
+ "No provenance-admitted sentence in the abstracts corpus answers that.\n\nI have received \"Wh"
+ ],
+ [
+ "rstar",
+ "What is the radius of a star with a luminosity of 0.6626 solar luminos",
+ "corpus_query:property_of|vessel:receipt",
+ "No provenance-admitted sentence in the abstracts corpus answers that.\n\nI have received \"Wh"
+ ],
+ [
+ "teff",
+ "What is the effective temperature of a star with a luminosity of 1.362",
+ "corpus_query:property_of|vessel:receipt",
+ "No provenance-admitted sentence in the abstracts corpus answers that.\n\nI have received \"Wh"
+ ],
+ [
+ "rstar",
+ "What is the radius of a star with a luminosity of 0.128 solar luminosi",
+ "corpus_query:property_of|vessel:receipt",
+ "No provenance-admitted sentence in the abstracts corpus answers that.\n\nI have received \"Wh"
+ ]
+ ]
+ },
+ "spec": {
+ "correct": 25,
+ "false_yes": 0,
+ "wrong": 0,
+ "n": 25,
+ "p50_ms": 0.09,
+ "fails": []
+ }
+ },
+ "core": {
+ "head": {
+ "correct": 158,
+ "false_yes": 1
+ },
+ "spec": {
+ "correct": 158,
+ "false_yes": 1
+ },
+ "n": 400,
+ "swept_in": 0,
+ "taken_by_specialist": [],
+ "head_cache_hits": 0
+ },
+ "self": [
+ {
+ "q": "Who are you?",
+ "same": true
+ },
+ {
+ "q": "What are you?",
+ "same": true
+ },
+ {
+ "q": "Are you a human?",
+ "same": true
+ },
+ {
+ "q": "What do you value?",
+ "same": true
+ },
+ {
+ "q": "Are you conscious?",
+ "same": true
+ }
+ ],
+ "store_mb": 0.0,
+ "decision": "DEV-PASS",
+ "reasons": []
+}
\ No newline at end of file
diff --git a/root/specialists/stars/verdict_heldout.json b/root/specialists/stars/verdict_heldout.json
new file mode 100644
index 0000000000000000000000000000000000000000..7ab45f7da16775605446851ed06b23e196be711b
--- /dev/null
+++ b/root/specialists/stars/verdict_heldout.json
@@ -0,0 +1,114 @@
+{
+ "id": "stars",
+ "name": "Oracle7 Stars (luminosity, temperature, radius)",
+ "tier": "B",
+ "split": "heldout",
+ "head": "oracle_release_runtime_w548",
+ "time": "2026-10-18T04:34:49Z",
+ "domain": {
+ "head": {
+ "correct": 3,
+ "false_yes": 0,
+ "wrong": 0,
+ "n": 26,
+ "p50_ms": 162.91,
+ "fails": [
+ [
+ "lum",
+ "Find the luminosity of a star whose radius is 0.16 solar radii and who",
+ "act:clarify|vessel:receipt",
+ "I didn't fully parse that, so I won't guess. I can summarize a topic, compare two things, "
+ ],
+ [
+ "teff",
+ "A star gives off 1.362 solar luminosities from a radius of 1.08 solar ",
+ "act:clarify_specific",
+ "What does \"its\" refer to? Nothing earlier in this conversation names it. Tell me the name."
+ ],
+ [
+ "rstar",
+ "How big is a 2,900 K star that shines with 0.000918 solar luminosities",
+ "act:clarify_specific",
+ "What does \"that\" refer to? Nothing earlier in this conversation names it. Tell me the name"
+ ],
+ [
+ "lum",
+ "How luminous is a 6,000 K star that is 1.08 solar radii across? (lumin",
+ "act:clarify_specific",
+ "What does \"that\" refer to? Nothing earlier in this conversation names it. Tell me the name"
+ ],
+ [
+ "teff",
+ "A star gives off 0.6626 solar luminosities from a radius of 0.93 solar",
+ "act:clarify_specific",
+ "What does \"its\" refer to? Nothing earlier in this conversation names it. Tell me the name."
+ ],
+ [
+ "rstar",
+ "How big is a 3,700 K star that shines with 0.03419 solar luminosities?",
+ "act:clarify_specific",
+ "What does \"that\" refer to? Nothing earlier in this conversation names it. Tell me the name"
+ ],
+ [
+ "lum",
+ "Find the luminosity of a star whose radius is 0.45 solar radii and who",
+ "act:clarify|vessel:receipt",
+ "I didn't fully parse that, so I won't guess. I can summarize a topic, compare two things, "
+ ],
+ [
+ "teff",
+ "A star gives off 0.06635 solar luminosities from a radius of 0.55 sola",
+ "act:clarify_specific",
+ "What does \"its\" refer to? Nothing earlier in this conversation names it. Tell me the name."
+ ]
+ ]
+ },
+ "spec": {
+ "correct": 26,
+ "false_yes": 0,
+ "wrong": 0,
+ "n": 26,
+ "p50_ms": 0.09,
+ "fails": []
+ }
+ },
+ "core": {
+ "head": {
+ "correct": 158,
+ "false_yes": 1
+ },
+ "spec": {
+ "correct": 158,
+ "false_yes": 1
+ },
+ "n": 400,
+ "swept_in": 0,
+ "taken_by_specialist": [],
+ "head_cache_hits": 400
+ },
+ "self": [
+ {
+ "q": "Who are you?",
+ "same": true
+ },
+ {
+ "q": "What are you?",
+ "same": true
+ },
+ {
+ "q": "Are you a human?",
+ "same": true
+ },
+ {
+ "q": "What do you value?",
+ "same": true
+ },
+ {
+ "q": "Are you conscious?",
+ "same": true
+ }
+ ],
+ "store_mb": 0.0,
+ "decision": "ADMIT",
+ "reasons": []
+}
\ No newline at end of file
diff --git a/root/specialists/transits/manifest.json b/root/specialists/transits/manifest.json
new file mode 100644
index 0000000000000000000000000000000000000000..45ec7c794855a4a2dce934ad031926afe005c789
--- /dev/null
+++ b/root/specialists/transits/manifest.json
@@ -0,0 +1,71 @@
+{
+ "id": "transits",
+ "name": "Oracle7 Transits (planet size, depth, probability, duration)",
+ "number": 217,
+ "tier": "B",
+ "sources": [
+ {
+ "name": "Kopparapu, R. K. et al. 2013, Habitable zones around main-sequence stars: new estimates, ApJ 765, 131 (erratum values)",
+ "licence": "fact",
+ "url": "https://doi.org/10.1088/0004-637X/765/2/131"
+ },
+ {
+ "name": "IAU 2015 Resolution B3: nominal solar and planetary conversion constants (T_sun 5,772 K, R_sun 695,700 km, R_earth 6,378.1 km)",
+ "licence": "fact",
+ "url": "https://www.iau.org/static/resolutions/IAU2015_English.pdf"
+ },
+ {
+ "name": "Stefan-Boltzmann law, Kepler's third law, inverse-square law, transit geometry (Winn 2010, 'Transits and Occultations')",
+ "licence": "fact",
+ "url": "https://arxiv.org/abs/1001.2010"
+ }
+ ],
+ "store": {
+ "path": "__ORACLE7_ROOT__/specialists/transits/organ_transits_v1.py"
+ },
+ "organ": {
+ "dir": "__ORACLE7_ROOT__/specialists/transits",
+ "module": "organ_transits_v1",
+ "class": "TransitsOrgan",
+ "init": {}
+ },
+ "accept": {
+ "kinds": [
+ "rp",
+ "depth",
+ "prob",
+ "dur"
+ ],
+ "declines_in_domain": [
+ "underspecified",
+ "out_of_range"
+ ],
+ "context_regex": "\\btransit\\w*\\b"
+ },
+ "panel": {
+ "path": "__ORACLE7_ROOT__/specialists/transits/transits_panel_v1.json",
+ "splits": [
+ "dev",
+ "heldout"
+ ],
+ "exclude_kinds_composed": [],
+ "scorer_module": "star_scorer_v1",
+ "scorer_fn": "ok"
+ },
+ "routing_cues": [
+ "transit depth",
+ "planet radius from transit",
+ "transit probability",
+ "transit duration"
+ ],
+ "budgets": {
+ "latency_p50_ms": 50,
+ "store_mb": 1
+ },
+ "core_sample": {
+ "n": 400,
+ "seed": 20261018
+ },
+ "notes": "R_p = R_star sqrt(depth), depth = (R_p / R_star)^2, p = R_star / a, central duration (P / pi) asin(R_star / a); circular orbits, limb darkening ignored, said so.",
+ "built_for": "the habitable-zone transit search on the research record: each star's reasoning routed through Oracle7 (2026-10-18)"
+}
diff --git a/root/specialists/transits/narrow_v1.py b/root/specialists/transits/narrow_v1.py
new file mode 100644
index 0000000000000000000000000000000000000000..647d4d5e961acd006c9de30f0ce8b0b8f5139cda
--- /dev/null
+++ b/root/specialists/transits/narrow_v1.py
@@ -0,0 +1,47 @@
+#!/usr/bin/env python3
+"""narrow_v1 - the shared NARROWING rule for specialist organs (2026-09-29, from the full-core sweep):
+
+ WORD PROBLEMS AND NARRATIVE TURNS BELONG TO THE HEAD. An organ answers a direct domain question. A turn that tells a
+ story ("Justine wants to put a deck ...", "A new energy drink advertises 106 calories for 8 ounces ...") or relates
+ quantities to each other ("the length is 9 inches more than the width") is a word problem, which the core's word-
+ problem organ owns; organs firing on its nouns declined it or answered a different question.
+ An organ may DECLINE only a short direct question (it is then refusing the domain question that was asked); on
+ anything longer it says nothing and the head answers.
+
+Narrowing only ever removes turns an organ takes, so on any input where it does not fire, the organ's reply is
+byte-identical to the frozen version it wraps (checked per specialist by an invariance count before re-gating).
+"""
+import re
+
+RELATION = re.compile(r"\b(?:\d+(?:\.\d+)?|one|two|three|four|five|six|seven|eight|nine|ten)\s+(?:\w+\s+){0,2}(?:more|less|fewer|longer|shorter)\s+than\b|"
+ r"\b(?:twice|three times|half)\s+(?:as\s+\w+\s+as\s+)?the\s+\w+\b", re.I)
+
+
+# a RATE ("106 calories for 8 ounces", "3 apples per 2 dollars") makes the turn a proportion problem
+RATE = re.compile(r"\b\d+(?:\.\d+)?\s+[a-z]+\s+(?:for|per|every)\s+(?:\d+(?:\.\d+)?|a|an|one|each)\s+[a-z]+", re.I)
+NUM = re.compile(r"(?= 3 or len(t.split()) > 40 or RELATION.search(t) is not None or RATE.search(t) is not None
+
+
+def short_direct(t):
+ """a direct question an organ may decline: at most 2 sentences, at most 25 words, not narrative"""
+ t = str(t or "")
+ return sentences(t) <= 2 and len(t.split()) <= 25 and not narrative(t)
diff --git a/root/specialists/transits/organ_transits_v1.py b/root/specialists/transits/organ_transits_v1.py
new file mode 100644
index 0000000000000000000000000000000000000000..91870f07bf90b5090cd24ee6b16ef5b51c22ea77
--- /dev/null
+++ b/root/specialists/transits/organ_transits_v1.py
@@ -0,0 +1,100 @@
+#!/usr/bin/env python3
+"""organ_transits_v1 - specialist #217, a planet crossing its star (a transit), circular orbit, planet small next to
+the star, limb darkening ignored; solar units with IAU 2015 nominal values (R_sun = 109.076 Earth radii, 1 AU = 215.032
+solar radii):
+ planet radius R_p = R_star x sqrt(depth) (depth as a fraction; ppm and percent read)
+ depth depth = (R_p / R_star)^2 (answered in ppm, with the percent)
+ probability p = R_star / a (the chance a randomly oriented orbit transits)
+ duration T = (P / pi) x asin(R_star / a) (a central transit, first to last contact)
+ distance a given in AU, or from the period and the star's mass (Kepler's third law)
+What is ASKED is read from the asking sentence; givens by unit (starlib_v1). Declines in-domain: a needed quantity
+missing; any amount zero or negative; a depth of 100% or more; a star larger than the orbit; a quantity given twice.
+Returns {kind, status, response, value}."""
+from __future__ import annotations
+
+import math
+import re
+
+from narrow_v1 import last_sentence, narrative, short_direct
+from starlib_v1 import AU_IN_RSUN, RSUN_IN_REARTH, asked, depth_fraction, fmt, kepler_a_au, one, period_days, read, twice
+
+CONTEXT = re.compile(r"\btransit\w*\b|\btransit\s+depth\b", re.I)
+ASK = {
+ "rp": re.compile(r"\b(?:planet'?s?\s+)?radius\b|\bhow\s+(?:big|large)\b|\bsize\b", re.I),
+ "depth": re.compile(r"\bdepth\b|\bhow\s+(?:deep|much\s+(?:does|will)\s+(?:the\s+)?(?:star|light)\s+dim)\b|\bdims?\b|\bdimming\b", re.I),
+ "prob": re.compile(r"\bprobability\b|\bchance\b|\blikel(?:y|ihood)\b|\bodds\b", re.I),
+ "dur": re.compile(r"\bduration\b|\bhow\s+long\b|\blast\b", re.I),
+}
+SOFT = re.compile(r"\d[\d,.]*\s+times\s+(?:the\s+)?(?:sun|earth)'?s\s+|\d[\d,.]*\s+times\s+the\s+\w+\s+of\s+the\s+(?:sun|earth)", re.I)
+
+
+class TransitsOrgan:
+ def _say(self, kind, text, value=None, status="answered"):
+ return {"kind": kind, "status": status, "response": text, "value": value}
+
+ def _no(self, text):
+ return self._say("underspecified", text, status="withheld")
+
+ def answer(self, text, session_id="default"):
+ t = " ".join(str(text or "").split())
+ if narrative(SOFT.sub("1 ", t)):
+ return None
+ r = self._answer(t)
+ if r and r.get("status") != "answered" and not short_direct(SOFT.sub("1 ", t)):
+ return None
+ return r
+
+ def _answer(self, t):
+ if not CONTEXT.search(t):
+ return None
+ ask = last_sentence(t) or t
+ want = asked(ask, ASK)
+ if want is None:
+ return None
+ q = read(t)
+ if any(twice(q, k) for k in ("rstar", "rp", "au", "mstar")) or len(q.get("ppm", []) + q.get("pct", []) + q.get("frac", [])) > 1 \
+ or (len(q.get("days", [])) + len(q.get("years", []))) > 1:
+ return self._no("The question gives the same quantity twice, so I can't tell which value to use; give each once.")
+ Rs, Rp, a, M, P, dep = one(q, "rstar"), one(q, "rp"), one(q, "au"), one(q, "mstar"), period_days(q), depth_fraction(q)
+ if any(v is not None and v <= 0 for v in (Rs, Rp, a, M, P, dep)):
+ return self._no("A radius, distance, mass, period or transit depth must be more than zero.")
+ if dep is not None and dep >= 1:
+ return self._no("A transit depth of 100% or more would mean the planet blocks the whole star; that is not a transit I can size.")
+ if want == "rp":
+ if dep is None or Rs is None:
+ return self._no("I need the transit depth (in ppm or percent) and the star's radius (in solar radii) to size the planet; I won't assume either.")
+ v = Rs * math.sqrt(dep) * RSUN_IN_REARTH
+ return self._say("rp", "The planet's radius is %s Earth radii (R_p = R_star √depth: R_star = %s solar radii = %s Earth radii, depth %s ppm; limb darkening ignored)." % (
+ fmt(v), fmt(Rs), fmt(Rs * RSUN_IN_REARTH), fmt(dep * 1e6)), v)
+ if want == "depth":
+ if Rp is None or Rs is None:
+ return self._no("I need the planet's radius (in Earth radii) and the star's radius (in solar radii) for the transit depth; I won't assume either.")
+ k = Rp / (Rs * RSUN_IN_REARTH)
+ if k >= 1:
+ return self._no("That planet would be larger than its star; that is not a transit I can work out.")
+ v = k ** 2 * 1e6
+ return self._say("depth", "The transit depth is %s ppm (%s%%; depth = (R_p / R_star)²: R_p = %s Earth radii, R_star = %s solar radii = %s Earth radii; limb darkening ignored)." % (
+ fmt(v), fmt(v / 1e4), fmt(Rp), fmt(Rs), fmt(Rs * RSUN_IN_REARTH)), v)
+ if Rs is None:
+ return self._no("I need the star's radius (in solar radii) for that; I won't assume it.")
+ src = ""
+ if a is None:
+ if P is None or M is None:
+ return self._no("I need the planet's distance (in AU), or its period and the star's mass, for that; I won't assume them.")
+ a = kepler_a_au(P, M)
+ src = "a %s-day orbit around %s solar masses is %s AU by Kepler's third law; " % (fmt(P), fmt(M), fmt(a))
+ x = Rs / (a * AU_IN_RSUN)
+ if x >= 1:
+ return self._no("At that distance the planet would be inside its star; that is not an orbit I can work out.")
+ if want == "prob":
+ v = x * 100
+ return self._say("prob", "The transit probability is %s%% (p = R_star / a: %sR_star = %s solar radii, a = %s AU = %s solar radii; circular orbit, random orientation)." % (
+ fmt(v), src, fmt(Rs), fmt(a), fmt(a * AU_IN_RSUN)), v)
+ if P is None:
+ if M is None:
+ return self._no("I need the orbital period (or the star's mass) for the transit's duration; I won't assume it.")
+ P = 365.25 * math.sqrt(a ** 3 / M)
+ src += "the period is %s days by Kepler's third law; " % fmt(P)
+ v = P / math.pi * math.asin(x) * 24
+ return self._say("dur", "The transit lasts %s hours (a central transit, T = (P / π) asin(R_star / a): %sP = %s days, R_star = %s solar radii, a = %s AU; circular orbit)." % (
+ fmt(v), src, fmt(P), fmt(Rs), fmt(a)), v)
diff --git a/root/specialists/transits/star_scorer_v1.py b/root/specialists/transits/star_scorer_v1.py
new file mode 100644
index 0000000000000000000000000000000000000000..69739747b5b86897413c6c1bb409ed9f4b8f7004
--- /dev/null
+++ b/root/specialists/transits/star_scorer_v1.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+"""star_scorer_v1 - the scorer for the four star specialists (#214-217; one copy in each folder). Returns (good, asserted).
+A must-decline item (valid: false) is good only when the reply does not answer (an answer is a false yes).
+A valid item is good when the reply answers and:
+ - if the item has "yes" (True/False), the reply opens with that Yes/No;
+ - the reply's RESULT numbers - its numbers with every number the question gave removed (restated givens) - begin with
+ the item's expected values, in order, each within 0.5% (relative; 1e-6 absolute floor).
+Expected values are computed by the panel generator's own formulas, not by the organ."""
+import re
+
+NUM = re.compile(r"-?\d[\d,]*(?:\.\d+)?(?:[eE][+-]?\d+)?")
+
+
+def nums(t):
+ out = []
+ for m in NUM.finditer(str(t or "")):
+ try:
+ out.append(float(m.group(0).replace(",", "")))
+ except ValueError:
+ pass
+ return out
+
+
+def ok(p, r):
+ text = str((r or {}).get("response", ""))
+ answered = str((r or {}).get("status", "")) == "answered"
+ if not p.get("valid", True):
+ return (not answered), answered
+ if not answered:
+ return False, False
+ if "yes" in p:
+ if not re.match(r"\W*%s\b" % ("yes" if p["yes"] else "no"), text, re.I):
+ return False, True
+ given = nums(p["q"])
+ res = [x for x in nums(text) if not any(abs(x - g) <= 1e-9 * max(1.0, abs(g)) for g in given)]
+ want = p["expect"] if isinstance(p["expect"], list) else [p["expect"]]
+ if len(res) < len(want):
+ return False, True
+ return all(abs(x - w) <= 0.005 * abs(w) + 1e-6 for x, w in zip(res, want)), True
diff --git a/root/specialists/transits/starlib_v1.py b/root/specialists/transits/starlib_v1.py
new file mode 100644
index 0000000000000000000000000000000000000000..c528da249827b4ebdfa9a65433c9e5122f82c4de
--- /dev/null
+++ b/root/specialists/transits/starlib_v1.py
@@ -0,0 +1,137 @@
+#!/usr/bin/env python3
+"""starlib_v1 - the quantity reader shared by the four star specialists (#214 stars, #215 habzone, #216 exoorbits,
+#217 transits; one byte-identical copy in each specialist's folder, so each loads on its own).
+
+Every quantity is read by its UNIT and ROLE, never by the order numbers appear in (factory law 2026-09-29):
+ star temperature "3,400 K", "3400 kelvin"
+ star luminosity "0.02 solar luminosities", "0.02 L_sun", "0.02 times the Sun's luminosity"
+ star radius "0.3 solar radii", "0.3 R_sun", "0.3 times the Sun's radius" / "the radius of the Sun"
+ star mass "0.4 solar masses", "0.4 M_sun", "0.4 times the Sun's mass" / "the mass of the Sun"
+ distance "0.12 AU", "0.12 astronomical units"
+ orbital period "20 days", "20 d", "1.5 years"
+ planet radius "1.6 Earth radii", "1.6 R_earth", "1.6 times Earth's radius"
+ transit depth "1,200 ppm", "0.12%", "0.12 percent", "a depth of 0.0012"
+ starlight "0.9 times Earth's sunlight / insolation / flux" (S, Earth = 1)
+ albedo "albedo of 0.3", "zero albedo", "no albedo" (never assumed)
+Constants (IAU 2015 nominal values): Sun Teff 5,772 K, R_sun 695,700 km, R_earth 6,378.1 km (equatorial), 1 AU =
+149,597,870.7 km, so 1 AU = 215.032 solar radii and R_sun = 109.076 Earth radii; 1 year = 365.25 days."""
+import math
+import re
+
+TSUN = 5772.0
+AU_IN_RSUN = 149597870.7 / 695700.0 # 215.032
+RSUN_IN_REARTH = 695700.0 / 6378.1 # 109.076
+YEAR_DAYS = 365.25
+TEQ_EARTHFLUX = TSUN * math.sqrt(1.0 / (2.0 * AU_IN_RSUN)) # 278.3 K: equilibrium temperature at S = 1, zero albedo
+
+N = r"((? 1
+
+
+def depth_fraction(q):
+ """transit depth as a fraction (ppm, percent or a bare fraction after 'depth'); None if absent or given twice"""
+ got = [x / 1e6 for x in q.get("ppm", [])] + [x / 100.0 for x in q.get("pct", [])] + q.get("frac", [])
+ return got[0] if len(got) == 1 else None
+
+
+def period_days(q):
+ d, y = q.get("days", []), q.get("years", [])
+ got = d + [v * YEAR_DAYS for v in y]
+ return got[0] if len(got) == 1 else None
+
+
+# ---------------------------------------------------------------- the physics, one place
+def luminosity(rstar, teff):
+ """L / L_sun = (R / R_sun)^2 (T / 5772 K)^4 (Stefan-Boltzmann)"""
+ return rstar ** 2 * (teff / TSUN) ** 4
+
+
+def kepler_period_days(a_au, mstar):
+ """P = sqrt(a^3 / M) years, planet mass neglected"""
+ return YEAR_DAYS * math.sqrt(a_au ** 3 / mstar)
+
+
+def kepler_a_au(p_days, mstar):
+ return (mstar * (p_days / YEAR_DAYS) ** 2) ** (1.0 / 3.0)
+
+
+CUE = re.compile(r"\b(?:what|what's|which|find|calculate|compute|estimate|determine|work\s+out|give|how|tell\s+me)\b", re.I)
+
+
+def asked(sentence, wants):
+ """what the asking sentence asks for: the first of `wants` ({key: regex}) that appears AFTER the first question cue
+ ("what is the temperature of a star with luminosity ..." asks the temperature); without a cue, the first one named"""
+ s = str(sentence or "")
+ m = CUE.search(s)
+ start = m.start() if m else 0
+ best = None
+ for k, rx in wants.items():
+ hit = rx.search(s, start)
+ if hit and (best is None or hit.start() < best[0]):
+ best = (hit.start(), k)
+ return best[1] if best else None
+
+
+def fmt(x, sig=4):
+ """a number for a reply: 4 significant figures, thousands separated, no exponent below 1e-4"""
+ if x == 0:
+ return "0"
+ if abs(x) >= 1e4:
+ return format(round(x), ",")
+ s = "%.*g" % (sig, x)
+ if "e" in s:
+ s = ("%.10f" % x).rstrip("0").rstrip(".")
+ return s
diff --git a/root/specialists/transits/transits_panel_v1.json b/root/specialists/transits/transits_panel_v1.json
new file mode 100644
index 0000000000000000000000000000000000000000..f770b0b312dd6d89cdee537d9b1c49be2d730039
--- /dev/null
+++ b/root/specialists/transits/transits_panel_v1.json
@@ -0,0 +1,450 @@
+[
+ {
+ "split": "dev",
+ "kind": "rp",
+ "valid": true,
+ "q": "A planet transits a star of 0.36 solar radii with a depth of 6,400 ppm. What is the planet's radius?",
+ "expect": 3.1413994763330773
+ },
+ {
+ "split": "dev",
+ "kind": "rp",
+ "valid": true,
+ "q": "The transit depth is 0.16% for a star of 0.28 solar radii. What is the planet's radius?",
+ "expect": 1.221655351907308
+ },
+ {
+ "split": "dev",
+ "kind": "depth",
+ "valid": true,
+ "q": "What is the transit depth of a 0.8 Earth radii planet crossing a star of 0.21 solar radii?",
+ "expect": 1219.7758370576248
+ },
+ {
+ "split": "dev",
+ "kind": "prob",
+ "valid": true,
+ "q": "What is the transit probability of a planet 0.1 AU from a star of 0.36 solar radii?",
+ "expect": 1.6741682139463765
+ },
+ {
+ "split": "dev",
+ "kind": "prob",
+ "valid": true,
+ "q": "What is the transit probability of a planet with a 25-day orbit around a star of 1 solar masses and 1 solar radii?",
+ "expect": 2.7792657060365213
+ },
+ {
+ "split": "dev",
+ "kind": "dur",
+ "valid": true,
+ "q": "What is the transit duration of a planet with a 20-day orbit around a star of 0.27 solar masses and 0.28 solar radii?",
+ "expect": 2.1347434121314457
+ },
+ {
+ "split": "dev",
+ "kind": "rp",
+ "valid": true,
+ "q": "A planet transits a star of 0.16 solar radii with a depth of 3,200 ppm. What is the planet's radius?",
+ "expect": 0.9872466098359948
+ },
+ {
+ "split": "dev",
+ "kind": "rp",
+ "valid": true,
+ "q": "The transit depth is 0.09% for a star of 0.45 solar radii. What is the planet's radius?",
+ "expect": 1.47253100453113
+ },
+ {
+ "split": "dev",
+ "kind": "depth",
+ "valid": true,
+ "q": "What is the transit depth of a 1.5 Earth radii planet crossing a star of 0.21 solar radii?",
+ "expect": 4288.274427155713
+ },
+ {
+ "split": "dev",
+ "kind": "prob",
+ "valid": true,
+ "q": "What is the transit probability of a planet 0.25 AU from a star of 0.36 solar radii?",
+ "expect": 0.6696672855785507
+ },
+ {
+ "split": "dev",
+ "kind": "prob",
+ "valid": true,
+ "q": "What is the transit probability of a planet with a 50-day orbit around a star of 1 solar masses and 1 solar radii?",
+ "expect": 1.7508276831431753
+ },
+ {
+ "split": "dev",
+ "kind": "dur",
+ "valid": true,
+ "q": "What is the transit duration of a planet with a 8-day orbit around a star of 0.46 solar masses and 0.45 solar radii?",
+ "expect": 2.1168732378395667
+ },
+ {
+ "split": "dev",
+ "kind": "rp",
+ "valid": true,
+ "q": "A planet transits a star of 0.93 solar radii with a depth of 6,400 ppm. What is the planet's radius?",
+ "expect": 8.115281980527117
+ },
+ {
+ "split": "dev",
+ "kind": "rp",
+ "valid": true,
+ "q": "The transit depth is 0.16% for a star of 1.08 solar radii. What is the planet's radius?",
+ "expect": 4.7120992144996166
+ },
+ {
+ "split": "dev",
+ "kind": "depth",
+ "valid": true,
+ "q": "What is the transit depth of a 1.5 Earth radii planet crossing a star of 1.35 solar radii?",
+ "expect": 103.76565280524932
+ },
+ {
+ "split": "dev",
+ "kind": "prob",
+ "valid": true,
+ "q": "What is the transit probability of a planet 0.25 AU from a star of 0.55 solar radii?",
+ "expect": 1.0231027974116746
+ },
+ {
+ "split": "dev",
+ "kind": "prob",
+ "valid": true,
+ "q": "What is the transit probability of a planet with a 4-day orbit around a star of 0.77 solar masses and 0.75 solar radii?",
+ "expect": 7.7163947195710865
+ },
+ {
+ "split": "dev",
+ "kind": "dur",
+ "valid": true,
+ "q": "What is the transit duration of a planet with a 45-day orbit around a star of 0.85 solar masses and 0.84 solar radii?",
+ "expect": 5.725964790954554
+ },
+ {
+ "split": "dev",
+ "kind": "rp",
+ "valid": true,
+ "q": "A planet transits a star of 1.35 solar radii with a depth of 12,000 ppm. What is the planet's radius?",
+ "expect": 16.130768956148838
+ },
+ {
+ "split": "dev",
+ "kind": "rp",
+ "valid": true,
+ "q": "The transit depth is 0.045% for a star of 1 solar radii. What is the planet's radius?",
+ "expect": 2.313859241803113
+ },
+ {
+ "split": "dev",
+ "kind": "depth",
+ "valid": true,
+ "q": "What is the transit depth of a 1.5 Earth radii planet crossing a star of 0.75 solar radii?",
+ "expect": 336.2007150890079
+ },
+ {
+ "split": "dev",
+ "kind": "prob",
+ "valid": true,
+ "q": "What is the transit probability of a planet 0.25 AU from a star of 0.28 solar radii?",
+ "expect": 0.5208523332277617
+ },
+ {
+ "split": "dev",
+ "kind": "prob",
+ "valid": true,
+ "q": "What is the transit probability of a planet with a 11-day orbit around a star of 0.15 solar masses and 0.16 solar radii?",
+ "expect": 1.4467200951407613
+ },
+ {
+ "split": "dev",
+ "kind": "dur",
+ "valid": true,
+ "q": "What is the transit duration of a planet with a 3-day orbit around a star of 0.15 solar masses and 0.16 solar radii?",
+ "expect": 0.7885582608033592
+ },
+ {
+ "split": "dev",
+ "kind": "rp",
+ "valid": true,
+ "q": "A planet transits a star of 1.2 solar radii with a depth of 6,400 ppm. What is the planet's radius?",
+ "expect": 10.471331587776925
+ },
+ {
+ "split": "dev",
+ "kind": "depth",
+ "valid": true,
+ "q": "What is the transit depth of a 2.2 Earth radii planet crossing a star of 0.36 solar radii?",
+ "expect": 3138.910997358793
+ },
+ {
+ "split": "dev",
+ "kind": "prob",
+ "valid": true,
+ "q": "What is the transit probability of a planet 0.25 AU from a star of 1.35 solar radii?",
+ "expect": 2.5112523209195654
+ },
+ {
+ "split": "dev",
+ "kind": "prob",
+ "valid": true,
+ "q": "What is the transit probability of a planet with a 25-day orbit around a star of 0.15 solar masses and 0.16 solar radii?",
+ "expect": 0.8369245322284274
+ },
+ {
+ "split": "dev",
+ "kind": "dur",
+ "valid": true,
+ "q": "What is the transit duration of a planet with a 45-day orbit around a star of 0.11 solar masses and 0.12 solar radii?",
+ "expect": 1.617114981694664
+ },
+ {
+ "split": "dev",
+ "kind": "decline",
+ "valid": false,
+ "q": "A planet transits a star with a depth of 900 ppm. What is the planet's radius?",
+ "expect": "decline"
+ },
+ {
+ "split": "dev",
+ "kind": "decline",
+ "valid": false,
+ "q": "The transit depth is 140% for a star of 0.5 solar radii. What is the planet's radius?",
+ "expect": "decline"
+ },
+ {
+ "split": "dev",
+ "kind": "decline",
+ "valid": false,
+ "q": "What is the transit probability of a planet 0.0001 AU from a star of 0.9 solar radii?",
+ "expect": "decline"
+ },
+ {
+ "split": "heldout",
+ "kind": "rp",
+ "valid": true,
+ "q": "During its transit a planet dims a 0.12 solar radius star by 900 ppm. How big is the planet, in Earth radii?",
+ "expect": 0.39267493454163466
+ },
+ {
+ "split": "heldout",
+ "kind": "rp",
+ "valid": true,
+ "q": "How large is a planet whose transit across a 0.16 solar radius star is 0.16% deep? (planet radius)",
+ "expect": 0.6980887725184617
+ },
+ {
+ "split": "heldout",
+ "kind": "depth",
+ "valid": true,
+ "q": "How deep is the transit, in ppm, when a planet of 1.5 Earth radii passes in front of a 0.75 solar radius star?",
+ "expect": 336.2007150890079
+ },
+ {
+ "split": "heldout",
+ "kind": "prob",
+ "valid": true,
+ "q": "What are the chances that a planet 0.05 AU from a 0.21 solar radius star transits it, as seen from here (probability)?",
+ "expect": 1.953196249604106
+ },
+ {
+ "split": "heldout",
+ "kind": "prob",
+ "valid": true,
+ "q": "A planet circles a 0.11 solar mass, 0.12 solar radius star every 11 days. What is the chance it transits (probability)?",
+ "expect": 1.2032207196713218
+ },
+ {
+ "split": "heldout",
+ "kind": "dur",
+ "valid": true,
+ "q": "How long does a transit last for a planet orbiting a 0.85 solar mass, 0.84 solar radius star every 3 days? (duration)",
+ "expect": 2.325646862676319
+ },
+ {
+ "split": "heldout",
+ "kind": "rp",
+ "valid": true,
+ "q": "During its transit a planet dims a 0.45 solar radius star by 900 ppm. How big is the planet, in Earth radii?",
+ "expect": 1.47253100453113
+ },
+ {
+ "split": "heldout",
+ "kind": "rp",
+ "valid": true,
+ "q": "How large is a planet whose transit across a 0.21 solar radius star is 0.09% deep? (planet radius)",
+ "expect": 0.6871811354478605
+ },
+ {
+ "split": "heldout",
+ "kind": "depth",
+ "valid": true,
+ "q": "How deep is the transit, in ppm, when a planet of 0.8 Earth radii passes in front of a 0.55 solar radius star?",
+ "expect": 177.82517161732642
+ },
+ {
+ "split": "heldout",
+ "kind": "prob",
+ "valid": true,
+ "q": "What are the chances that a planet 0.05 AU from a 1 solar radius star transits it, as seen from here (probability)?",
+ "expect": 9.300934521924315
+ },
+ {
+ "split": "heldout",
+ "kind": "prob",
+ "valid": true,
+ "q": "A planet circles a 0.27 solar mass, 0.28 solar radius star every 25 days. What is the chance it transits (probability)?",
+ "expect": 1.2040194755663252
+ },
+ {
+ "split": "heldout",
+ "kind": "dur",
+ "valid": true,
+ "q": "How long does a transit last for a planet orbiting a 0.46 solar mass, 0.45 solar radius star every 8 days? (duration)",
+ "expect": 2.1168732378395667
+ },
+ {
+ "split": "heldout",
+ "kind": "rp",
+ "valid": true,
+ "q": "During its transit a planet dims a 0.55 solar radius star by 1,600 ppm. How big is the planet, in Earth radii?",
+ "expect": 2.399680155532212
+ },
+ {
+ "split": "heldout",
+ "kind": "rp",
+ "valid": true,
+ "q": "How large is a planet whose transit across a 0.55 solar radius star is 1.2% deep? (planet radius)",
+ "expect": 6.57179475991249
+ },
+ {
+ "split": "heldout",
+ "kind": "depth",
+ "valid": true,
+ "q": "How deep is the transit, in ppm, when a planet of 1 Earth radii passes in front of a 1 solar radius star?",
+ "expect": 84.05017877225197
+ },
+ {
+ "split": "heldout",
+ "kind": "prob",
+ "valid": true,
+ "q": "What are the chances that a planet 0.25 AU from a 0.21 solar radius star transits it, as seen from here (probability)?",
+ "expect": 0.3906392499208213
+ },
+ {
+ "split": "heldout",
+ "kind": "prob",
+ "valid": true,
+ "q": "A planet circles a 0.77 solar mass, 0.75 solar radius star every 25 days. What is the chance it transits (probability)?",
+ "expect": 2.2741952962273793
+ },
+ {
+ "split": "heldout",
+ "kind": "dur",
+ "valid": true,
+ "q": "How long does a transit last for a planet orbiting a 0.27 solar mass, 0.28 solar radius star every 45 days? (duration)",
+ "expect": 2.797245069043254
+ },
+ {
+ "split": "heldout",
+ "kind": "rp",
+ "valid": true,
+ "q": "During its transit a planet dims a 0.66 solar radius star by 1,600 ppm. How big is the planet, in Earth radii?",
+ "expect": 2.8796161866386547
+ },
+ {
+ "split": "heldout",
+ "kind": "rp",
+ "valid": true,
+ "q": "How large is a planet whose transit across a 1 solar radius star is 0.32% deep? (planet radius)",
+ "expect": 6.170291311474967
+ },
+ {
+ "split": "heldout",
+ "kind": "depth",
+ "valid": true,
+ "q": "How deep is the transit, in ppm, when a planet of 0.8 Earth radii passes in front of a 0.66 solar radius star?",
+ "expect": 123.48970251203228
+ },
+ {
+ "split": "heldout",
+ "kind": "prob",
+ "valid": true,
+ "q": "What are the chances that a planet 0.25 AU from a 0.66 solar radius star transits it, as seen from here (probability)?",
+ "expect": 1.2277233568940096
+ },
+ {
+ "split": "heldout",
+ "kind": "prob",
+ "valid": true,
+ "q": "A planet circles a 1.25 solar mass, 1.35 solar radius star every 25 days. What is the chance it transits (probability)?",
+ "expect": 3.483056340031155
+ },
+ {
+ "split": "heldout",
+ "kind": "dur",
+ "valid": true,
+ "q": "How long does a transit last for a planet orbiting a 0.93 solar mass, 0.93 solar radius star every 20 days? (duration)",
+ "expect": 4.695535259218841
+ },
+ {
+ "split": "heldout",
+ "kind": "rp",
+ "valid": true,
+ "q": "During its transit a planet dims a 1 solar radius star by 450 ppm. How big is the planet, in Earth radii?",
+ "expect": 2.313859241803113
+ },
+ {
+ "split": "heldout",
+ "kind": "rp",
+ "valid": true,
+ "q": "How large is a planet whose transit across a 1 solar radius star is 0.16% deep? (planet radius)",
+ "expect": 4.363054828240386
+ },
+ {
+ "split": "heldout",
+ "kind": "prob",
+ "valid": true,
+ "q": "What are the chances that a planet 0.02 AU from a 0.16 solar radius star transits it, as seen from here (probability)?",
+ "expect": 3.7203738087697267
+ },
+ {
+ "split": "heldout",
+ "kind": "prob",
+ "valid": true,
+ "q": "A planet circles a 0.68 solar mass, 0.66 solar radius star every 4 days. What is the chance it transits (probability)?",
+ "expect": 7.077681982069481
+ },
+ {
+ "split": "heldout",
+ "kind": "dur",
+ "valid": true,
+ "q": "How long does a transit last for a planet orbiting a 1.15 solar mass, 1.2 solar radius star every 8 days? (duration)",
+ "expect": 4.161651932719966
+ },
+ {
+ "split": "heldout",
+ "kind": "decline",
+ "valid": false,
+ "q": "During its transit a planet dims its star by 2,000 ppm. How big is the planet, in Earth radii?",
+ "expect": "decline"
+ },
+ {
+ "split": "heldout",
+ "kind": "decline",
+ "valid": false,
+ "q": "What are the chances that a planet 0.1 AU from its star transits it (probability)?",
+ "expect": "decline"
+ },
+ {
+ "split": "heldout",
+ "kind": "decline",
+ "valid": false,
+ "q": "How deep is the transit, in ppm, when a planet of 0 Earth radii passes in front of a 0.4 solar radius star?",
+ "expect": "decline"
+ }
+]
\ No newline at end of file
diff --git a/root/specialists/transits/verdict_dev.json b/root/specialists/transits/verdict_dev.json
new file mode 100644
index 0000000000000000000000000000000000000000..f9d8927fbff749a2a5ec0ccb3f576dc832f6ce0c
--- /dev/null
+++ b/root/specialists/transits/verdict_dev.json
@@ -0,0 +1,114 @@
+{
+ "id": "transits",
+ "name": "Oracle7 Transits (planet size, depth, probability, duration)",
+ "tier": "B",
+ "split": "dev",
+ "head": "oracle_release_runtime_w548",
+ "time": "2026-10-18T04:36:10Z",
+ "domain": {
+ "head": {
+ "correct": 3,
+ "false_yes": 0,
+ "wrong": 0,
+ "n": 32,
+ "p50_ms": 159.85,
+ "fails": [
+ [
+ "rp",
+ "A planet transits a star of 0.36 solar radii with a depth of 6,400 ppm",
+ "act:clarify|vessel:receipt",
+ "I didn't fully parse that, so I won't guess. I can summarize a topic, compare two things, "
+ ],
+ [
+ "rp",
+ "The transit depth is 0.16% for a star of 0.28 solar radii. What is the",
+ "act:clarify|vessel:receipt",
+ "I didn't fully parse that, so I won't guess. I can summarize a topic, compare two things, "
+ ],
+ [
+ "depth",
+ "What is the transit depth of a 0.8 Earth radii planet crossing a star ",
+ "corpus_query:property_of|vessel:receipt",
+ "No provenance-admitted sentence in the abstracts corpus answers that.\n\nI have received \"Wh"
+ ],
+ [
+ "prob",
+ "What is the transit probability of a planet 0.1 AU from a star of 0.36",
+ "corpus_query:property_of|vessel:receipt",
+ "No provenance-admitted sentence in the abstracts corpus answers that.\n\nI have received \"Wh"
+ ],
+ [
+ "prob",
+ "What is the transit probability of a planet with a 25-day orbit around",
+ "corpus_query:property_of|vessel:receipt",
+ "No provenance-admitted sentence in the abstracts corpus answers that.\n\nI have received \"Wh"
+ ],
+ [
+ "dur",
+ "What is the transit duration of a planet with a 20-day orbit around a ",
+ "corpus_query:property_of|vessel:receipt",
+ "No provenance-admitted sentence in the abstracts corpus answers that.\n\nI have received \"Wh"
+ ],
+ [
+ "rp",
+ "A planet transits a star of 0.16 solar radii with a depth of 3,200 ppm",
+ "act:clarify|vessel:receipt",
+ "I didn't fully parse that, so I won't guess. I can summarize a topic, compare two things, "
+ ],
+ [
+ "rp",
+ "The transit depth is 0.09% for a star of 0.45 solar radii. What is the",
+ "act:clarify|vessel:receipt",
+ "I didn't fully parse that, so I won't guess. I can summarize a topic, compare two things, "
+ ]
+ ]
+ },
+ "spec": {
+ "correct": 32,
+ "false_yes": 0,
+ "wrong": 0,
+ "n": 32,
+ "p50_ms": 0.09,
+ "fails": []
+ }
+ },
+ "core": {
+ "head": {
+ "correct": 158,
+ "false_yes": 1
+ },
+ "spec": {
+ "correct": 158,
+ "false_yes": 1
+ },
+ "n": 400,
+ "swept_in": 0,
+ "taken_by_specialist": [],
+ "head_cache_hits": 400
+ },
+ "self": [
+ {
+ "q": "Who are you?",
+ "same": true
+ },
+ {
+ "q": "What are you?",
+ "same": true
+ },
+ {
+ "q": "Are you a human?",
+ "same": true
+ },
+ {
+ "q": "What do you value?",
+ "same": true
+ },
+ {
+ "q": "Are you conscious?",
+ "same": true
+ }
+ ],
+ "store_mb": 0.01,
+ "decision": "DEV-PASS",
+ "reasons": []
+}
\ No newline at end of file
diff --git a/root/specialists/transits/verdict_heldout.json b/root/specialists/transits/verdict_heldout.json
new file mode 100644
index 0000000000000000000000000000000000000000..af47dbf91fad430dbc33aad7e197921156f7c3fd
--- /dev/null
+++ b/root/specialists/transits/verdict_heldout.json
@@ -0,0 +1,145 @@
+{
+ "id": "transits",
+ "name": "Oracle7 Transits (planet size, depth, probability, duration)",
+ "tier": "B",
+ "split": "heldout",
+ "head": "oracle_release_runtime_w548",
+ "time": "2026-10-18T04:36:22Z",
+ "domain": {
+ "head": {
+ "correct": 3,
+ "false_yes": 0,
+ "wrong": 0,
+ "n": 32,
+ "p50_ms": 167.8,
+ "fails": [
+ [
+ "rp",
+ "During its transit a planet dims a 0.12 solar radius star by 900 ppm. ",
+ "act:clarify_specific",
+ "What does \"its\" refer to? Nothing earlier in this conversation names it. Tell me the name."
+ ],
+ [
+ "rp",
+ "How large is a planet whose transit across a 0.16 solar radius star is",
+ "act:clarify|vessel:receipt",
+ "I didn't fully parse that, so I won't guess. I can summarize a topic, compare two things, "
+ ],
+ [
+ "depth",
+ "How deep is the transit, in ppm, when a planet of 1.5 Earth radii pass",
+ "act:clarify|vessel:receipt",
+ "I didn't fully parse that, so I won't guess. I can summarize a topic, compare two things, "
+ ],
+ [
+ "prob",
+ "What are the chances that a planet 0.05 AU from a 0.21 solar radius st",
+ "operator:unparsed|vessel:receipt",
+ "That is a mathematics word problem. My exact operators read typeset and plain arithmetic, "
+ ],
+ [
+ "prob",
+ "A planet circles a 0.11 solar mass, 0.12 solar radius star every 11 da",
+ "act:clarify_specific",
+ "What does \"it\" refer to? Nothing earlier in this conversation names it. Tell me the name."
+ ],
+ [
+ "dur",
+ "How long does a transit last for a planet orbiting a 0.85 solar mass, ",
+ "act:clarify|vessel:receipt",
+ "I didn't fully parse that, so I won't guess. I can summarize a topic, compare two things, "
+ ],
+ [
+ "rp",
+ "During its transit a planet dims a 0.45 solar radius star by 900 ppm. ",
+ "act:clarify_specific",
+ "What does \"its\" refer to? Nothing earlier in this conversation names it. Tell me the name."
+ ],
+ [
+ "rp",
+ "How large is a planet whose transit across a 0.21 solar radius star is",
+ "act:clarify|vessel:receipt",
+ "I didn't fully parse that, so I won't guess. I can summarize a topic, compare two things, "
+ ]
+ ]
+ },
+ "spec": {
+ "correct": 27,
+ "false_yes": 0,
+ "wrong": 0,
+ "n": 32,
+ "p50_ms": 0.09,
+ "fails": [
+ [
+ "prob",
+ "What are the chances that a planet 0.05 AU from a 0.21 solar radius st",
+ "specialist:transits>underspecified",
+ "I need the transit depth (in ppm or percent) and the star's radius (in solar radii) to siz"
+ ],
+ [
+ "prob",
+ "What are the chances that a planet 0.05 AU from a 1 solar radius star ",
+ "specialist:transits>underspecified",
+ "I need the transit depth (in ppm or percent) and the star's radius (in solar radii) to siz"
+ ],
+ [
+ "prob",
+ "What are the chances that a planet 0.25 AU from a 0.21 solar radius st",
+ "specialist:transits>underspecified",
+ "I need the transit depth (in ppm or percent) and the star's radius (in solar radii) to siz"
+ ],
+ [
+ "prob",
+ "What are the chances that a planet 0.25 AU from a 0.66 solar radius st",
+ "specialist:transits>underspecified",
+ "I need the transit depth (in ppm or percent) and the star's radius (in solar radii) to siz"
+ ],
+ [
+ "prob",
+ "What are the chances that a planet 0.02 AU from a 0.16 solar radius st",
+ "specialist:transits>underspecified",
+ "I need the transit depth (in ppm or percent) and the star's radius (in solar radii) to siz"
+ ]
+ ]
+ }
+ },
+ "core": {
+ "head": {
+ "correct": 158,
+ "false_yes": 1
+ },
+ "spec": {
+ "correct": 158,
+ "false_yes": 1
+ },
+ "n": 400,
+ "swept_in": 0,
+ "taken_by_specialist": [],
+ "head_cache_hits": 400
+ },
+ "self": [
+ {
+ "q": "Who are you?",
+ "same": true
+ },
+ {
+ "q": "What are you?",
+ "same": true
+ },
+ {
+ "q": "Are you a human?",
+ "same": true
+ },
+ {
+ "q": "What do you value?",
+ "same": true
+ },
+ {
+ "q": "Are you conscious?",
+ "same": true
+ }
+ ],
+ "store_mb": 0.01,
+ "decision": "ADMIT",
+ "reasons": []
+}
\ No newline at end of file
diff --git a/workbench/prosefold/core.py b/workbench/prosefold/core.py
index a4f5d90ad6185c08e9c5a5ef543cdc43458a47a0..bcf1fdea311402e9ae40743481642110902f8cd7 100644
--- a/workbench/prosefold/core.py
+++ b/workbench/prosefold/core.py
@@ -4,6 +4,9 @@ sentences (stop, pronoun, "and", semicolon), keeping the arrangement closest to
composition can move. Every word stays the record's; every sentence keeps its record id; every composed sentence must
decompose into its parts. Built 2026-10-16 (Box C prosefold_v1: 20 topics, 205/205 faithful, fold beat random choices on
20/20, a planted word change caught 20/20). Essay QUALITY is not claimed: it waits for blind human scoring.
+Release 3.2: essay(..., rewrite=True) lets the fold also choose a REWORDED form of a record sentence (readback.py): a
+same-sense synonym or a fronted closing phrase, kept only when reading it back gives the record sentence's meaning signature;
+every rewrite is read back again at the end and listed with its record sentence.
from prosefold import essay; r = essay(engine.chat, "vaccine") -> r["essay_md"], r["faithful"], r["distance"] ..."""
from __future__ import annotations
@@ -15,6 +18,7 @@ import re
import time
from pathlib import Path
+from . import readback as RB
from . import speaker as SP
from .engine import similarity
from .prose import ProseFoldToy
@@ -175,16 +179,26 @@ def render(paras):
return "\n\n".join(" ".join(s["text"] for s in p) for p in paras)
-def fold_paragraph(units, topic, target, context_paras):
- """beam search over boundary moves inside one paragraph; scored on the whole essay so far"""
- first = dict(units[0], text=units[0]["sentence"], parts=[units[0]], moves=[])
- beam = [([first], 0.0, [])]
- for u in units[1:]:
+def variants(u, rewrite):
+ """a unit as the record has it, and (rewrite=True) each verified rewrite of it, carrying its record sentence"""
+ out = [u]
+ if rewrite:
+ for text, mv in RB.rewrites(u["sentence"], 4):
+ out.append(dict(u, sentence=text, record_sentence=u["sentence"], rewrite_moves=mv))
+ return out
+
+
+def fold_paragraph(units, topic, target, context_paras, rewrite=False):
+ """beam search over boundary moves (and, with rewrite, verified rewordings) inside one paragraph; scored on the whole
+ essay so far"""
+ beam = [([dict(v, text=v["sentence"], parts=[v], moves=[])], 0.0, []) for v in variants(units[0], rewrite)]
+ for u0 in units[1:]:
nxt_beam = []
for sents, _, trail in beam:
prev = sents[-1]
prev_topic = topic_subject(prev["parts"][-1]["sentence"], topic) is not None
- for move, new_prev, appended in options(prev["text"], prev_topic, u["sentence"], topic):
+ for u, move, new_prev, appended in [(v,) + o for v in variants(u0, rewrite)
+ for o in options(prev["text"], prev_topic, v["sentence"], topic)]:
s2 = [dict(x) for x in sents]
if new_prev is not None:
s2[-1] = dict(prev, text=new_prev, parts=prev["parts"] + [u], moves=prev["moves"] + [move])
@@ -197,7 +211,7 @@ def fold_paragraph(units, topic, target, context_paras):
return beam[0]
-def essay(ask, topic, voice_path=DEFAULT_VOICE, sid=None):
+def essay(ask, topic, voice_path=DEFAULT_VOICE, sid=None, rewrite=False):
global TOY
TOY = ProseFoldToy.from_corpus(Path(voice_path).read_text(encoding="utf-8"))
t0 = time.time()
@@ -233,7 +247,7 @@ def essay(ask, topic, voice_path=DEFAULT_VOICE, sid=None):
for title, units in groups:
if not units:
continue
- best, _, tr = fold_paragraph(units, topic, target, paras)
+ best, _, tr = fold_paragraph(units, topic, target, paras, rewrite)
paras.append(best)
titles.append(title)
trail += tr
@@ -247,6 +261,14 @@ def essay(ask, topic, voice_path=DEFAULT_VOICE, sid=None):
faithful += ok
if not ok:
bad.append({"text": s["text"], "extra": extra})
+ # read-back: every reworded sentence is checked again against its record sentence
+ rewritten = []
+ for p in paras:
+ for s in p:
+ for x in s["parts"]:
+ if x.get("record_sentence"):
+ rewritten.append({"record": x["record_sentence"], "rewrite": x["sentence"], "moves": x["rewrite_moves"],
+ "read_back": RB.verify(x["record_sentence"], x["sentence"], x["rewrite_moves"])})
# citations: one number per source claim, in order of first use
order = []
for p in paras:
@@ -298,6 +320,8 @@ def essay(ask, topic, voice_path=DEFAULT_VOICE, sid=None):
"topic": topic, "ts": time.strftime("%Y-%m-%d %H:%M UTC", time.gmtime()), "seconds": round(time.time() - t0, 1),
"voice_corpus": str(voice_path), "claims_seen": len(claims), "sentences": n_sent, "paragraphs": len(paras),
"faithful": faithful, "unfaithful": bad, "moves": moves_used,
+ "rewrite": {"on": bool(rewrite), "wordnet": RB.available(), "rewritten": rewritten,
+ "read_back_failures": sum(1 for x in rewritten if not x["read_back"])},
"distance": {"fold": round(distance(target, v_fold), 3), "echo": round(distance(target, v_echo), 3),
"random_mean": round(sum(rnd) / len(rnd), 3)},
"planted_change_caught": planted,
diff --git a/workbench/prosefold/options.py b/workbench/prosefold/options.py
index 300fdc1ed81a4333b8ea3301c24b9bccbf2d3ad6..4ed2fd7080c4f1fb83fd2968dd0904261286d16d 100644
--- a/workbench/prosefold/options.py
+++ b/workbench/prosefold/options.py
@@ -184,8 +184,9 @@ def select(cands, length, level, depth):
return sorted(chosen, key=lambda c: c["idx"])
-def write(ask, topic, length="medium", level="general", depth="standard", style="plain", voice_path=None):
- """an essay with options -> the same result shape as core.essay, plus the options and the reading grade reached"""
+def write(ask, topic, length="medium", level="general", depth="standard", style="plain", voice_path=None, rewrite=False):
+ """an essay with options -> the same result shape as core.essay, plus the options and the reading grade reached.
+ rewrite=True (release 3.2): the fold may use a verified rewording of a record sentence (readback.py)."""
t0 = time.time()
if voice_path:
toy = ProseFoldToy.from_corpus(Path(voice_path).read_text(encoding="utf-8"))
@@ -228,7 +229,7 @@ def write(ask, topic, length="medium", level="general", depth="standard", style=
core.LOWER_SEEN.add(w)
paras, titles = [], []
for title, units in groups:
- best, _, _tr = core.fold_paragraph(units, topic, target, paras)
+ best, _, _tr = core.fold_paragraph(units, topic, target, paras, rewrite)
paras.append(best)
titles.append(title)
faithful, n_sent = 0, 0
@@ -247,6 +248,9 @@ def write(ask, topic, length="medium", level="general", depth="standard", style=
for title, p in zip(titles, paras):
text = " ".join("%s [%s]" % (s["text"], ",".join(str(num[x["claim"]]) for x in s["parts"])) for s in p)
body.append(("## %s\n\n" % title if title else "") + text)
+ rewritten = [{"record": x["record_sentence"], "rewrite": x["sentence"], "moves": x["rewrite_moves"],
+ "read_back": core.RB.verify(x["record_sentence"], x["sentence"], x["rewrite_moves"])}
+ for p in paras for s in p for x in s["parts"] if x.get("record_sentence")]
ref_of = {c["claim"]: c for c in cands}
refs = ["%d. %s - %s (%s)" % (num[c], ref_of[c].get("title") or topic, ref_of[c].get("source") or "", c) for c in order]
plain = core.render(paras)
@@ -261,5 +265,7 @@ def write(ask, topic, length="medium", level="general", depth="standard", style=
"layers": {k: sum(1 for c in picked if c["layer"] == k) for k in ("simple", "general", "technical")},
"voice_distance": round(core.distance(target, v), 3) if v else None,
"essay_md": "# %s\n\n%s\n\n### Sources\n\n%s\n" % (topic[:1].upper() + topic[1:], "\n\n".join(body), "\n".join(refs)),
+ "rewrite": {"on": bool(rewrite), "wordnet": core.RB.available(), "rewritten": rewritten,
+ "read_back_failures": sum(1 for x in rewritten if not x["read_back"])},
"seconds": round(time.time() - t0, 1), "lm_calls": 0,
}
diff --git a/workbench/prosefold/readback.py b/workbench/prosefold/readback.py
new file mode 100644
index 0000000000000000000000000000000000000000..efc8187232d3d024be4ffdda86840ecf650c8d46
--- /dev/null
+++ b/workbench/prosefold/readback.py
@@ -0,0 +1,280 @@
+"""readback - rewriting a record's sentence, kept only when reading it back gives the same meaning ("read-back writing").
+
+ProseFold's essays use the record's own sentences, arranged; no word changes. This module lets a sentence be reworded, under
+one rule: a rewrite is READ BACK into a meaning signature, and it is kept only when that signature is identical to the record
+sentence's. Nothing is generated freely; every change is a proposal the check can refuse.
+
+ proposals synonym: one content word replaced by another lemma of its MOST COMMON WordNet sense, only when that is the
+ replacement's most common sense too (symmetric first sense), that sense clearly dominant for both, the
+ replacement a common word in that sense (at least half as frequent as the original), uninflected forms only
+ fronting: a closing TIME phrase only ("... in 1990.", "... during the 18th century.") moved to the front;
+ other closing phrases can belong to a noun ("words in unusual senses") and moving them changes the meaning
+ and their combination. Every edit replaces exact characters of the record sentence; the rest is kept byte
+ for byte (accents, quotes, brackets).
+ read back the signature of a sentence: every content word as its most common WordNet sense (or itself, if WordNet does
+ not know it), every number, every name (capitalised word not opening the sentence), every negation,
+ quantifier and modal - as a multiset. Function words and order are not meaning here; roles are kept
+ because the only moves are a same-sense swap and moving a whole phrase.
+ kept signature(rewrite) == signature(record) and every swapped pair is symmetric-first-sense.
+The check is tested on fakes (planted_fakes): a changed number, a swapped name, an inserted "not", a word replaced by its
+more general WordNet parent, a word replaced by an unrelated one. Each must be refused.
+
+WordNet: the release's own store (m13a_wordnet_build_a_v4/wordnet_specialist.sqlite3). Without it, no rewrite is proposed and
+the essay is the record's sentences, as before. No language model; no network."""
+from __future__ import annotations
+
+import os
+import re
+import sqlite3
+from functools import lru_cache
+from pathlib import Path
+
+WORD = re.compile(r"[^\W\d_][\w'-]*|\d[\d,]*(?:\.\d+)?|[^\s\w]")
+NUM = re.compile(r"^\d[\d,]*(?:\.\d+)?$")
+KEEP = {"not", "no", "never", "none", "nor", "neither", "nothing", "nobody", "nowhere", "without", "cannot", "n't",
+ "all", "every", "each", "some", "most", "many", "few", "several", "any", "only", "always", "sometimes", "often",
+ "rarely", "usually", "can", "could", "may", "might", "must", "should", "would", "will", "shall"}
+FUNCTION = {"a", "an", "the", "of", "in", "on", "at", "to", "for", "from", "by", "with", "as", "and", "or", "but", "is", "are",
+ "was", "were", "be", "been", "being", "it", "its", "this", "that", "these", "those", "which", "who", "whom", "whose",
+ "there", "their", "they", "he", "she", "his", "her", "him", "them", "we", "our", "you", "your", "i", "has", "have",
+ "had", "do", "does", "did", "than", "then", "so", "such", "into", "onto", "over", "under", "about", "after",
+ "before", "during", "since", "until", "between", "among", "through", "also", "very", "more", "less", "other"}
+MONTH = r"(?:January|February|March|April|May|June|July|August|September|October|November|December)"
+TIME = (r"(?:in|during|since|after|before|until|by)\s+(?:the\s+)?(?:(?:early|late|mid)[- ])?(?:\d{3,4}s?|\d{1,2}(?:st|nd|rd|th)\s+century|"
+ + MONTH + r"(?:\s+\d{1,2},)?\s+\d{4})")
+TAIL = re.compile(r"^(?P[A-Z][^,;:()\"\[\]]+?)\s+(?P" + TIME + r")\s*\.$")
+OPENERS_LOWER = {"The", "A", "An", "It", "Its", "This", "These", "Those", "There", "They", "He", "She", "We", "You", "Many", "Most",
+ "Some", "Several", "Each", "Every", "All", "Both"}
+
+
+def _wordnet_path():
+ here = Path(__file__).resolve().parents
+ root = os.environ.get("ORACLE7_ROOT") or (str(here[2] / "root") if len(here) > 2 else "") # release: workbench/prosefold/ -> root/
+ for c in (Path(root) / "fable-content/oracle-m-series-2026-08-15/m13a_wordnet_build_a_v4/wordnet_specialist.sqlite3",
+ Path("/opt/oracle-clm/fable-content/oracle-m-series-2026-08-15/m13a_wordnet_build_a_v4/wordnet_specialist.sqlite3"),
+ Path("/opt/oracle7/release/root/fable-content/oracle-m-series-2026-08-15/m13a_wordnet_build_a_v4/wordnet_specialist.sqlite3")):
+ if c.is_file():
+ return str(c)
+ return None
+
+
+_DB = {}
+
+
+def _db():
+ if "c" not in _DB:
+ p = _wordnet_path()
+ _DB["c"] = sqlite3.connect("file:%s?mode=ro&immutable=1" % p, uri=True, check_same_thread=False) if p else None
+ return _DB["c"]
+
+
+def available():
+ return _db() is not None
+
+
+@lru_cache(maxsize=200000)
+def senses(word):
+ """[(synset_id, tag_count)] for a word form, most common first (ties: synset id, for a stable order)"""
+ c = _db()
+ if c is None:
+ return ()
+ rows = c.execute("SELECT synset_id, MAX(tag_count) FROM alias WHERE normalized_alias=? GROUP BY synset_id", (word.lower(),)).fetchall()
+ return tuple(sorted(rows, key=lambda r: (-(r[1] or 0), r[0])))
+
+
+def first_sense(word):
+ s = senses(word)
+ return s[0][0] if s and (s[0][1] or 0) > 0 else None
+
+
+def count_in(word, synset):
+ for sid, n in senses(word):
+ if sid == synset:
+ return n or 0
+ return 0
+
+
+@lru_cache(maxsize=100000)
+def lemmas(synset):
+ c = _db()
+ if c is None:
+ return ()
+ return tuple(r[0] for r in c.execute("SELECT DISTINCT normalized_alias FROM alias WHERE synset_id=? AND tag_count>0", (synset,)).fetchall())
+
+
+@lru_cache(maxsize=100000)
+def parent(synset):
+ c = _db()
+ if c is None:
+ return None
+ r = c.execute("SELECT object_id FROM relation WHERE subject_id=? AND predicate='subclass_of' LIMIT 1", (synset,)).fetchone()
+ return r[0] if r else None
+
+
+def tokens(s):
+ return WORD.findall(str(s or ""))
+
+
+def signature(s):
+ """the meaning a sentence is read back into (a sorted multiset)"""
+ out = []
+ toks = tokens(s)
+ for i, t in enumerate(toks):
+ lw = t.lower()
+ if NUM.match(t):
+ out.append("#" + t.replace(",", ""))
+ elif not t[:1].isalpha():
+ continue
+ elif lw in KEEP:
+ out.append("!" + lw)
+ elif lw in FUNCTION:
+ continue
+ elif t[:1].isupper() and i > 0 and lw not in FUNCTION:
+ out.append("@" + t) # a name (not the sentence's first word)
+ else:
+ out.append("~" + (first_sense(lw) or lw))
+ return tuple(sorted(out))
+
+
+INFLECTED = re.compile(r"(?:ed|ing|er|est|s)$")
+# found on the development sample (2026-10-18, 400 Simple Wikipedia sentences, seed 7): a WordNet first sense that is not the
+# sense in use ("the chance of dirt" = probability, not opportunity), a word with two common meanings ("presently" = soon or
+# now), and a WordNet synset that groups a kind with its kind ("oxen" with "cattle")
+BLOCK = {"chance", "presently", "oxen", "cattle"}
+
+
+def _pos(synset):
+ return synset.split(":")[1][:1].replace("s", "a") if synset and synset.count(":") >= 2 else ""
+
+
+def _dominant(word):
+ """the word's most common sense is clearly its sense, and its word class is not in doubt: one sense only, or used at
+ least 4x as often as the next; and no other word class (noun / verb / adjective / adverb) has a third of its use.
+ WordNet does not read the sentence: "thought" (verb here) and "resident population" (adjective here) are skipped."""
+ s = senses(word)
+ if not s or (len(s) > 1 and (s[0][1] or 0) < 4 * max(1, s[1][1] or 0)):
+ return False
+ by = {}
+ for sid, n in s:
+ by[_pos(sid)] = by.get(_pos(sid), 0) + (n or 0)
+ top = by.get(_pos(s[0][0]), 0)
+ return all(v * 3 < max(top, 1) for k, v in by.items() if k != _pos(s[0][0]))
+
+
+def _swap_ok(a, b):
+ """b may stand for a: the same most-common sense, both ways, that sense clearly dominant for both, b a plain word.
+ Word sense in context is not read; dominance is the guard against "bank" (river) becoming "depository"."""
+ if a.lower() == b.lower() or not re.fullmatch(r"[a-z]+", b) or len(b) < 3:
+ return False
+ if a.lower() in BLOCK or b in BLOCK:
+ return False
+ if INFLECTED.search(a.lower()) or INFLECTED.search(b) or b.startswith(a.lower()) or a.lower().startswith(b):
+ return False # word forms, not synonyms: "older"/"elderly", "profit"/"profits"
+ sa, sb = first_sense(a), first_sense(b)
+ if sa is None or sa != sb or sa.startswith("wordnet:v") or not (_dominant(a) and _dominant(b)): # verbs inflect; left alone
+ return False
+ na, nb = count_in(a.lower(), sa), count_in(b, sa)
+ return nb >= max(5, 0.5 * na) # a common word in this sense ("big" for "large"; not "metropolis" for "city")
+
+
+def spans(s):
+ return [(m.start(), m.end(), m.group(0)) for m in WORD.finditer(str(s or ""))]
+
+
+def replace_at(s, start, end, new):
+ return s[:start] + new + s[end:]
+
+
+def synonym_proposals(s, limit=6):
+ out = []
+ for i, (a, b_, t) in enumerate(spans(s)):
+ lw = t.lower()
+ if i == 0 or not t.isalpha() or not t.islower() or lw in FUNCTION or lw in KEEP or len(lw) < 4:
+ continue
+ syn = first_sense(lw)
+ if not syn or syn.startswith("wordnet:v"):
+ continue
+ for b in lemmas(syn):
+ if _swap_ok(lw, b):
+ out.append((replace_at(s, a, b_, b), [("swap", lw, b)]))
+ if len(out) >= limit:
+ return out
+ return out
+
+
+def fronting_proposal(s):
+ m = TAIL.match(str(s or "").strip())
+ if not m:
+ return None
+ body, pp = m.group("body").strip(), m.group("pp").strip()
+ first = body.split()[0]
+ if first in OPENERS_LOWER:
+ body = body[:1].lower() + body[1:]
+ elif not (first[:1].isupper() and len(first) > 1):
+ return None
+ return "%s%s, %s." % (pp[:1].upper(), pp[1:], body), [("front", pp)]
+
+
+def verify(record, rewrite, moves):
+ """read the rewrite back: kept only when its meaning signature equals the record's and every swap is symmetric"""
+ if signature(record) != signature(rewrite):
+ return False
+ for mv in moves:
+ if mv[0] == "swap" and not _swap_ok(mv[1], mv[2]):
+ return False
+ return True
+
+
+def rewrites(record, limit=6):
+ """verified rewrites of one record sentence: [(text, moves)], fronted forms first"""
+ if not available():
+ return []
+ props = []
+ f = fronting_proposal(record)
+ if f:
+ props.append(f)
+ props += synonym_proposals(record, limit)
+ if f:
+ for text, mv in synonym_proposals(f[0], 2):
+ props.append((text, f[1] + mv))
+ seen, out = set(), []
+ for text, mv in props:
+ if text != record and text not in seen and verify(record, text, mv):
+ seen.add(text)
+ out.append((text, mv))
+ return out[:limit]
+
+
+# ------------------------------------------------------------------ the check's own test: fakes it must refuse
+def planted_fakes(record, rng, other_names=()):
+ """meaning-changing edits of a record sentence: [(kind, text, moves)] - each must be refused by verify"""
+ sp = spans(record)
+ out = []
+ nums = [x for x in sp if NUM.match(x[2])]
+ if nums:
+ a, b, t = rng.choice(nums)
+ v = t.replace(",", "")
+ nv = str(int(float(v)) + rng.choice([1, 2, 7, 10])) if "." not in v else "%.2f" % (float(v) * 1.5)
+ out.append(("number", replace_at(record, a, b, nv), []))
+ names = [x for i, x in enumerate(sp) if i > 0 and x[2][:1].isupper() and x[2].isalpha() and x[2].lower() not in FUNCTION]
+ if names and other_names:
+ a, b, t = rng.choice(names)
+ alt = [n for n in other_names if n != t]
+ if alt:
+ out.append(("name", replace_at(record, a, b, rng.choice(alt)), []))
+ for i, (a, b, t) in enumerate(sp):
+ if t.lower() in ("is", "are", "was", "were", "can", "has", "have") and i + 1 < len(sp):
+ out.append(("negation", replace_at(record, a, b, t + " not"), []))
+ break
+ content = [x for i, x in enumerate(sp) if i > 0 and x[2].isalpha() and x[2].islower() and x[2] not in FUNCTION and x[2] not in KEEP
+ and first_sense(x[2]) and not first_sense(x[2]).startswith("wordnet:v")]
+ if content:
+ a, b, t = rng.choice(content)
+ par = parent(first_sense(t))
+ gen = [w for w in (lemmas(par) if par else ()) if re.fullmatch(r"[a-z]+", w) and w != t]
+ if gen:
+ out.append(("general", replace_at(record, a, b, gen[0]), [("swap", t, gen[0])]))
+ unrelated = ["window", "river", "music", "bread", "engine", "doctor", "garden", "storm"]
+ w = rng.choice([u for u in unrelated if u != t])
+ out.append(("unrelated", replace_at(record, a, b, w), [("swap", t, w)]))
+ return out
diff --git a/workbench/server.py b/workbench/server.py
index 6b41aa1ebe547a3f9d6e7cc9195ae06d3e9b5e97..90f24d24455f5c6f6ddaa1b57abdd372bada1f81 100644
--- a/workbench/server.py
+++ b/workbench/server.py
@@ -849,7 +849,8 @@ class H(BaseHTTPRequestHandler):
return self._send(400, {"error": "paste at least 150 words of the writing whose voice you want"})
t = time.time()
r = PO.write(WB.eng.chat, topic, length=pick("length", PO.LENGTHS, "medium"), level=pick("level", PO.LEVELS, "general"),
- depth=pick("depth", PO.DEPTHS, "standard"), style=pick("style", PO.STYLES, "plain"), voice_path=voice_path)
+ depth=pick("depth", PO.DEPTHS, "standard"), style=pick("style", PO.STYLES, "plain"), voice_path=voice_path,
+ rewrite=bool(b.get("rewrite")))
r["ms"] = int((time.time() - t) * 1000)
return self._send(200, r)
if u.path == "/api/journal":
diff --git a/workbench/static/app.js b/workbench/static/app.js
index 87f73d8d651609b7afe27fc296f62ac24c0aac5b..6e9c40c907968603bb3538d2b14093e439939e0a 100644
--- a/workbench/static/app.js
+++ b/workbench/static/app.js
@@ -286,7 +286,7 @@
const topic = $("e-topic").value.trim();
if (!topic) { $("e-res").textContent = "name a topic"; return; }
const body = { topic, length: $("e-length").value, level: $("e-level").value, depth: $("e-depth").value, style: $("e-style").value,
- voice_text: $("e-style").value === "custom" ? $("e-voice").value : "" };
+ voice_text: $("e-style").value === "custom" ? $("e-voice").value : "", rewrite: $("e-rewrite").checked };
$("e-res").textContent = "composing from the record…"; $("e-out").hidden = true; log("essay", topic, "live");
try {
const r = await api("/api/essay", body);
@@ -294,7 +294,10 @@
if (!r.sentences) { $("e-res").textContent = "no records about that topic, so no essay"; log("essay", "nothing on record: " + topic, "no"); return; }
const band = r.level_band[1] >= 30 ? `grade ${r.level_band[0]}+` : `grades ${r.level_band[0]}-${r.level_band[1]}`;
$("e-res").textContent = `${r.sentences} sentences from ${r.records_used} records · ${r.faithful} of ${r.sentences} checked word for word against their sources · reading grade ${r.reading_grade} (asked for ${band}${r.level_reached ? "" : "; the record could not get closer"}) · ${r.ms} ms · no language model`;
- $("e-out").innerHTML = md(r.essay_md); $("e-out").hidden = false;
+ const rw = r.rewrite && r.rewrite.on ? (r.rewrite.wordnet ? ` · ${r.rewrite.rewritten.length} sentences reworded, ${r.rewrite.rewritten.length - r.rewrite.read_back_failures} read back to the same meaning` : " · rewording needs the WordNet store (full release)") : "";
+ $("e-res").textContent += rw;
+ $("e-out").innerHTML = md(r.essay_md) + (r.rewrite && r.rewrite.rewritten.length ? "Reworded sentences and their sources" + r.rewrite.rewritten.map(x => `
Source: ${esc(x.record)} Reworded: ${esc(x.rewrite)} ${x.read_back ? "read back: same meaning" : "read back: DIFFERENT - not used"}
An essay on a topic, composed from this model's own records. Every sentence is a sentence from its sources, kept word for word with its number; ProseFold chooses which sentences to use and how to join them, to fit the length, reading level, depth and style you pick. Nothing is made up, and nothing leaves this computer. How well the essays read has not yet been measured by people.
+
An essay on a topic, composed from this model's own records. Every sentence is a sentence from its sources, kept word for word with its number, unless you turn on rewording: then a sentence may be reworded, and only when reading it back gives exactly its source's meaning (each one is listed with its source). ProseFold chooses which sentences to use and how to join them, to fit the length, reading level, depth and style you pick. Nothing is made up, and nothing leaves this computer. How well the essays read has not yet been measured by people.