Requirement 001 phase 6, S0: - Speech: the security class shows as "Sécurité" and is spoken as in maritime radio, "say-cure-ee-tay" three times. A hex run of 7 or more characters with a letter and a digit (commit, session, message ids) is spelled in the NATO alphabet; every other digit is spoken on its own in ICAO pronunciation (42305: four two tree zero fife). The spelling runs inside clip() before the length cut, so a phrase still fits SPEECH_LIMIT; the displayed text keeps its digits and hashes. - English: every orchestrator label, tooltip, message, error, script output, document and test string is English (Regeln -> Firewall, Hierarchie -> Hierarchy, Steuerung -> Controls, Ausführen -> Run, ...); the page declares lang="en" and formats dates as en-GB. Task filter ids are English. German stays only in the skill trigger phrases that quote the operator, and as identifiers: the view keys (view=) and the leitstand id. - tests/test_english.py scans every tracked text file for umlauts and German words, with an allowlist of the quoted trigger phrases. BREAKING CHANGE: the Stream search tokens art: and seit: are now kind: and since:; an old art: or seit: token counts as a plain word. Evidence: make test 657 OK, ruff and markdownlint clean (/tmp/s0-make-test-1.log); e2e 17 OK, none skipped (/tmp/s0-e2e-1.log); MEDUSA 86 files, 24 findings on reworded strings accepted after an independent skeptic (21 path:line, 0 rejected, no behaviour change in signing, approving, rules, denying, offices, events), rescan 0 open. 🎯 Fix: 93 % 💥 Break: 4 % 💸 Cost: 100 % ⏱️ Time: – 🛡️ Security: 100 % 🏆 Score: 89 🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 % Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
88 lines
4.4 KiB
Python
88 lines
4.4 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import subprocess
|
|
import unittest
|
|
|
|
from hub_support import AMBIENCE, STREAMS
|
|
|
|
SLOTS = [(0, 420, "Morning"), (0, 539, "Morning"), (0, 540, None), (1, 419, None), (5, 480, None), (5, 1380, "Night"), (6, 90, "Night"), (6, 120, None), (4, 1380, None), (6, 1380, None)]
|
|
PROBE = """
|
|
const A = require(process.argv[1]);
|
|
const at = (text) => Date.parse(text);
|
|
const pick = (body, keys) => Object.fromEntries(keys.map((key) => [key, body[key]]));
|
|
const surfaces = [];
|
|
for (const mode of ["light", "dark"]) {
|
|
for (const surface of ["bg", "panel"]) {
|
|
for (const sky of A.SKY.flatMap(([, top, bottom]) => [top, bottom])) {
|
|
const behind = A.mix(A.hex(sky), A.hex(A.THEMES[mode][surface]), A.GLASS);
|
|
surfaces.push({mode, surface, sky, text: A.contrast(A.hex(A.THEMES[mode].text), behind), muted: A.contrast(A.hex(A.THEMES[mode].muted), behind)});
|
|
}
|
|
}
|
|
}
|
|
const slots = JSON.parse(process.argv[3]).map(([day, minute]) => ({day, minute}));
|
|
console.log(JSON.stringify({
|
|
sun: pick(A.sun(at("1992-10-13T00:00:00Z"), 0, 0), ["ra", "dec", "eot"]),
|
|
solstice: A.sun(at("2024-06-20T20:51:00Z"), 0, 0).dec,
|
|
london: A.sun(at("2024-06-21T03:43:00Z"), 51.5074, -0.1278).alt,
|
|
moon: pick(A.moon(at("1992-04-12T00:00:00Z"), 0, 0), ["ra", "dec", "lambda", "beta", "distance", "lit", "limb", "waxing"]),
|
|
full: A.moon(at("2024-04-23T23:49:00Z"), 52.5, 13.4).lit,
|
|
fresh: A.moon(at("2024-04-08T18:21:00Z"), 52.5, 13.4).lit,
|
|
quarter: pick(A.moon(at("2024-04-15T19:13:00Z"), 52.5, 13.4), ["lit", "waxing"]),
|
|
sidereal: [A.sidereal(at("1987-04-10T00:00:00Z")), A.sidereal(at("1987-04-10T19:21:00Z"))],
|
|
stages: [-30, -15, -9, -3, 3, 20].map(A.stage),
|
|
modes: [-10, 3.9, 4, 40].map(A.modeAt),
|
|
black: A.contrast([0, 0, 0], [255, 255, 255]),
|
|
surfaces,
|
|
slot: A.slotOf(new Date(2024, 5, 21, 7, 30)),
|
|
playing: slots.map((slot) => (A.activeStream(JSON.parse(process.argv[2]), slot) || {name: null}).name),
|
|
}));
|
|
"""
|
|
|
|
|
|
class TestAmbience(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
command = ["node", "-e", PROBE, str(AMBIENCE), json.dumps(STREAMS), json.dumps([slot[:2] for slot in SLOTS])]
|
|
done = subprocess.run(command, capture_output=True, text=True, timeout=60, check=True)
|
|
cls.data = json.loads(done.stdout)
|
|
|
|
def test_the_sun_matches_meeus_and_noaa_references(self):
|
|
sun = self.data["sun"]
|
|
self.assertAlmostEqual(sun["ra"], 198.38083, delta=0.01)
|
|
self.assertAlmostEqual(sun["dec"], -7.78507, delta=0.01)
|
|
self.assertAlmostEqual(sun["eot"], 13.71, delta=0.1)
|
|
self.assertAlmostEqual(self.data["solstice"], 23.438, delta=0.01)
|
|
self.assertAlmostEqual(self.data["london"], -0.833, delta=0.3)
|
|
self.assertAlmostEqual(self.data["sidereal"][0], 197.693195, delta=0.001)
|
|
self.assertAlmostEqual(self.data["sidereal"][1], 128.7378734, delta=0.001)
|
|
|
|
def test_the_moon_matches_meeus_within_the_low_precision_series(self):
|
|
moon = self.data["moon"]
|
|
for key, expected, delta in (("lambda", 133.162655, 0.5), ("beta", -3.229126, 0.3), ("ra", 134.68847, 0.5), ("dec", 13.768368, 0.3), ("distance", 368409.7, 3000), ("lit", 0.6786, 0.01), ("limb", 285.0, 2.0)):
|
|
with self.subTest(key=key):
|
|
self.assertAlmostEqual(moon[key], expected, delta=delta)
|
|
self.assertTrue(moon["waxing"])
|
|
self.assertGreater(self.data["full"], 0.99)
|
|
self.assertLess(self.data["fresh"], 0.01)
|
|
self.assertAlmostEqual(self.data["quarter"]["lit"], 0.5, delta=0.03)
|
|
self.assertTrue(self.data["quarter"]["waxing"])
|
|
|
|
def test_sky_stages_modes_and_text_contrast_hold_in_every_sky(self):
|
|
self.assertEqual(self.data["stages"], ["night", "astronomical", "nautical", "civil", "golden", "day"])
|
|
self.assertEqual(self.data["modes"], ["dark", "dark", "light", "light"])
|
|
self.assertAlmostEqual(self.data["black"], 21.0, places=6)
|
|
self.assertEqual(len(self.data["surfaces"]), 88)
|
|
for row in self.data["surfaces"]:
|
|
with self.subTest(mode=row["mode"], surface=row["surface"], sky=row["sky"]):
|
|
self.assertGreaterEqual(row["text"], 7.0)
|
|
self.assertGreaterEqual(row["muted"], 4.5)
|
|
|
|
def test_streams_play_on_their_weekdays_and_hours(self):
|
|
self.assertEqual(self.data["slot"], {"day": 4, "minute": 450})
|
|
self.assertEqual(self.data["playing"], [slot[2] for slot in SLOTS])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|