diff --git a/.trinity/seals/automation_automation::crm_story_reel.json b/.trinity/seals/automation_automation::crm_story_reel.json index 60935727a0..2ce627b96d 100644 --- a/.trinity/seals/automation_automation::crm_story_reel.json +++ b/.trinity/seals/automation_automation::crm_story_reel.json @@ -1,19 +1,19 @@ { - "gen_hash_c": "sha256:7f35a6af45dc06ab73a300d630b184640b88bb2cebd1f7972df11c75a03f455b", - "gen_hash_rust": "sha256:99143b58e33f3594a8f4fbf3f530ca5f0dd25bcbdc6291899948e1f637233c1d", - "gen_hash_verilog": "sha256:ff86b4fc0c84f9cf3774c952811f19a4def4b464750794ab25660bf8baebe3b6", - "gen_hash_zig": "sha256:2c794f94a951ed30a45789a192382268836e442db6ca58cd67869e13137dd836", + "gen_hash_c": "sha256:d90e565306d6e8822a212f5ee3ec60bb6df1f191da79f71e54e08bbc9f559d0e", + "gen_hash_rust": "sha256:b55b70168fa2f1898dcaa7da73ea9b79ccd4e2babfc2d1648f3ce8b921bd53ba", + "gen_hash_verilog": "sha256:746b523f5605fb9cd26f9a07c30f4a94ea6ca4adae00fb0ceb81d80afe140048", + "gen_hash_zig": "sha256:402312033e41e50256748d7b30adb2087e71860d95bf035f6846cde341de48e0", "module": "automation::crm_story_reel", "ring": 12, - "sealed_at": "2026-10-03T23:03:02Z", + "sealed_at": "2026-10-04T04:29:15Z", "sealed_by": "t27c-bootstrap@0.4.0", - "spec_hash": "sha256:0ad474e7969112e5a63c062e7a19b5a09c0a35ee1f2ff1474e704113c92280a7", + "spec_hash": "sha256:b042ff61cc2f3e1a3a14e078e5c7eb1aa81cf53918876b0f744a8cb0f0acfa38", "spec_path": "specs/automation/crm-story-reel.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 55, - "total": 55 + "passed": 60, + "total": 60 } } \ No newline at end of file diff --git a/docs/now/2026-10-04-crm-story-reel-v23-house-marks-read-as-a-reader-reads-them.md b/docs/now/2026-10-04-crm-story-reel-v23-house-marks-read-as-a-reader-reads-them.md new file mode 100644 index 0000000000..4249d1d8e9 --- /dev/null +++ b/docs/now/2026-10-04-crm-story-reel-v23-house-marks-read-as-a-reader-reads-them.md @@ -0,0 +1,9 @@ +# NOW -- crm-story-reel v23: house marks read as a reader reads them (2026-10-04) + +## crm-story-reel v23: house marks read as a reader reads them (Closes #5897) + +- fold: the text and every house mark are folded the same way first -- NFKC (S3AI written with a superscript is s3ai), invisible characters dropped (INVISIBLES_DROPPED), one case, and each Cyrillic look-alike read as its Latin letter (FOLD_LOOKALIKES, LOOKALIKE_LATIN), so a Latin T in the Russian name and a Cyrillic Te in the English one both read as the house +- code_mark: the brand T27 is the house when it starts a word (CODE_MARK_PREFIX: t27, t27ai, t27_ai, T27.ai), not after a letter or digit (at27, 5t27) nor before a digit (T270); write to t27ai is now flagged +- product_mark: the product is ONE code token (PRODUCT_ONE_TOKEN): S3AI and S3AIs are the house, our S3 AI pipeline is S3 the storage and the word AI and names no house +- name_mark: a house name is the house inside a longer word (NAME_INSIDE_WORD: TrinityBot) and before any word (NAME_IN_ANY_CONTEXT): Trinity College stays flagged on purpose -- no rule on the next word clears the college without clearing Trinity AI or Write To Trinity AI, and a refused agent line is rewritten for free while a missed one is on a client's reel; her own typed line is still hers (own_words) +- test: no rule that clears a false positive clears a mark -- Trinity, Trinity S3AI, T27 and @t27ai_bot stay flagged; 60/60 spec tests, controls in the 999 binding diff --git a/specs/automation/crm-story-reel.t27 b/specs/automation/crm-story-reel.t27 index 99f6581b9c..87bef78b58 100644 --- a/specs/automation/crm-story-reel.t27 +++ b/specs/automation/crm-story-reel.t27 @@ -191,6 +191,24 @@ // Trinity" makes "Write to Trinity" hers. Every template line has a house-free client version, // kept by the host, and a client's list shows only those; any other line naming the house is // refused before anything is spent (REFUSED_BEFORE_SPEND, charged), as split-reel v7 refuses it. +// v23 (2026-10-04, 999#3564): the detector of the house's names missed two spellings and +// caught two things that are not the house. "write to t27ai" passed: T27 was read only as a +// whole word, so the handle and the domain slipped by. Trinity in Russian with a Latin T passed: +// a model writing Russian mixes letters that look alike, and the detector read code points, not +// what a reader sees. Both are now read as a reader reads them. The text and every mark are +// folded first -- NFKC (a superscript three is a 3), invisible characters dropped +// (INVISIBLES_DROPPED), one case, each Cyrillic letter that looks Latin read as that Latin letter +// (FOLD_LOOKALIKES, LOOKALIKE_LATIN) -- and then each mark is judged by what stands around it: +// a name anywhere, inside a longer word too (name_mark); T27 when it starts a word, t27ai and +// t27_ai included, not after a letter (at27) nor before a digit (T270) (code_mark, +// CODE_MARK_PREFIX). The product is one token (product_mark, PRODUCT_ONE_TOKEN): "our S3 AI +// pipeline" is S3 the storage and the word AI, not S3AI. "I studied at Trinity College" stays +// a house name (NAME_IN_ANY_CONTEXT). No rule on the word after a name clears the college and +// not the house: "Trinity" before a capital word is also "Trinity AI", "Trinity Bot" and every +// title-cased call ("Write To Trinity AI"). And the costs are not equal: a refusal costs a line +// the agent wrote, rewritten for free, never one she typed herself (own_words), while a miss +// puts the house's name on a client's reel. So a rule that clears a false positive must not +// clear a mark: the bare name, Trinity S3AI, T27 and @t27ai_bot stay flagged. // Delivered live on 2026-09-26 (two reels for one client, forwarded by the owner). On // 2026-09-27 the voice provider answered HTTP 402 (no credit): the voice leg is BLOCKED by // money, not by code, and Kie has no voice cloning to fall back to. @@ -201,7 +219,7 @@ module automation::crm_story_reel { pub const KIND : str = "automation"; pub const ID : str = "crm-story-reel"; pub const REPO : str = "999-multibots-telegraf"; - pub const VERSION : u8 = 22; + pub const VERSION : u8 = 23; // ---- Who qualifies ------------------------------------------------------------------ // Both halves, nothing else. A profile only overrides the look, the call and who pays. @@ -505,7 +523,7 @@ module automation::crm_story_reel { // ---- A client's reel carries the client's call (v22) -------------------------------------- // The house is named by BOT_SPOKEN_AS, its Russian spelling (kept by the host, this file // stays ASCII), HOUSE_DELIVERY_BOT (TEMPLATE_BUTTON is it with an at-sign), HOUSE_BRAND and - // HOUSE_PRODUCT (the host also reads its superscript spelling). One detector in the host + // HOUSE_PRODUCT (its superscript spelling too, after the fold of v23). One detector in the host // reads them all, for every spec that refuses a house name on a client's reel. pub const HOUSE_BRAND : str = "T27"; pub const HOUSE_PRODUCT : str = "S3AI"; @@ -529,6 +547,28 @@ module automation::crm_story_reel { pub const SPEAKS_REFUSED : u8 = 2; // a client turn naming another client pub const CLIENT_TURN_SPEAKS_CLIENT : bool = true; // a client turn's face is the client + // ---- The house's names, read as a reader reads them (v23) ---------------------------------- + // The text and every mark are folded the same way before they are compared: NFKC (the + // superscript three is a 3, a full-width letter is its letter), invisible characters dropped, + // one case, and each Cyrillic letter whose capital looks like a Latin capital read as that + // Latin letter (Te is T, Er is P, En is H; LOOKALIKE_LATIN lists the Latin side). Greek is not + // folded: the leak is a model writing Russian that mixes the two scripts it writes. Then each + // mark found is judged by what stands around it -- a name (name_mark), the brand as a code + // (code_mark), the product as one code token (product_mark). The rows of 999#3564 are vectors + // the host runs through its detector; the Russian row, Trinity in Cyrillic with a Latin T, is + // kept by the host, as this file stays ASCII. + pub const FOLD_LOOKALIKES : bool = true; // a Cyrillic look-alike reads as its Latin letter + pub const INVISIBLES_DROPPED : bool = true; // a zero-width or soft-hyphen character splits no mark + pub const LOOKALIKE_LATIN : str = "abcdehijkmopqstwxy"; // the Latin letters a Cyrillic one passes for + pub const NAME_INSIDE_WORD : bool = true; // TrinityBot, #trinity: still the name + pub const NAME_IN_ANY_CONTEXT : bool = true; // Trinity College too: no word after it tells which + pub const CODE_MARK_PREFIX : bool = true; // T27 starting a word -- t27ai, t27_ai -- is the brand + pub const PRODUCT_ONE_TOKEN : bool = true; // S3AI is one token; S3 AI is two words + pub const MARK_ROW_T27AI_EN : str = "write to t27ai"; // names the house: the brand starts a word + pub const MARK_ROW_S3_SPACE_EN : str = "our S3 AI pipeline"; // names nothing: S3 and AI + pub const MARK_ROW_COLLEGE_EN : str = "I studied at Trinity College"; // names the house + pub const MARK_ROW_TITLE_CASE_EN : str = "Write To Trinity AI"; // a title-cased call: the house + // ---- Every line of a client's reel (v22) --------------------------------------------------- // Every line is checked, the first as much as the last: the captions are the lines as // written (CAPTION_TEXT_FROM_LINE), so a line naming the house is a caption naming it too. @@ -1042,6 +1082,45 @@ module automation::crm_story_reel { return LINE_FROM_EDIT; } + // Whether a house name found in the folded text names the house: anywhere, inside a longer + // word too (TrinityBot, #trinity, the handle), and whatever word follows it -- Trinity + // College as much as Trinity AI, since no word after the name tells which Trinity it is. + fn name_mark(inside_word: bool, word_after: bool) -> bool { + if inside_word { + return NAME_INSIDE_WORD; + } + if word_after { + return NAME_IN_ANY_CONTEXT; + } + return true; + } + + // Whether a code found in the folded text is the house's: not when a letter or a digit + // stands right before it (at27) or a digit right after it (T270 is another number), and yes + // when it starts a word (t27ai, the handle and the domain: CODE_MARK_PREFIX). + fn code_mark(word_before: bool, digit_after: bool, letter_after: bool) -> bool { + if word_before || digit_after { + return false; + } + if letter_after { + return CODE_MARK_PREFIX; + } + return true; + } + + // Whether the product found in the folded text is the house's: a code like the brand + // (code_ok, from code_mark), written as one token. With a space inside it is two words -- + // "S3 AI" is S3 the storage and the word AI -- and names nothing (PRODUCT_ONE_TOKEN). + fn product_mark(code_ok: bool, space_inside: bool) -> bool { + if !code_ok { + return false; + } + if space_inside { + return !PRODUCT_ONE_TOKEN; + } + return true; + } + test "a client qualifies with a face and a voice, and with nothing less" { assert(FACE_REQUIRED); assert(VOICE_REQUIRED); @@ -1423,7 +1502,7 @@ module automation::crm_story_reel { } test "VERSION says what the header's last note says" { - assert(VERSION == 22); + assert(VERSION == 23); } test "the hook reel is a template of four studio shots with the call to the house bot" { @@ -1640,6 +1719,71 @@ module automation::crm_story_reel { assert(LINE_FROM_EDIT != LINE_FROM_PERSON); assert(LINE_FROM_TEMPLATE != LINE_FROM_PERSON); } + + test "a Cyrillic letter that looks Latin reads as that letter, and an invisible one splits no name" { + // The Russian row -- Trinity in Cyrillic with a Latin T -- folds to the Russian mark, + // folded the same way, so it is the bare name, and an agent's line with it is refused. + assert(FOLD_LOOKALIKES); + assert(INVISIBLES_DROPPED); + assert(LOOKALIKE_LATIN == "abcdehijkmopqstwxy"); + assert(name_mark(false, false)); + assert(line_way(name_mark(false, false), false, LINE_FROM_EDIT) == LINE_REFUSED); + assert(!charged(line_way(name_mark(false, false), false, LINE_FROM_EDIT) == LINE_REFUSED)); + } + + test "T27 starting a word is the brand: t27ai and t27_ai, not at27 nor T270" { + assert(MARK_ROW_T27AI_EN == "write to t27ai"); + assert(CODE_MARK_PREFIX); + assert(code_mark(false, false, true)); + assert(code_mark(false, false, false)); + assert(!code_mark(true, false, false)); + assert(!code_mark(true, false, true)); + assert(!code_mark(false, true, false)); + assert(line_way(code_mark(false, false, true), false, LINE_FROM_EDIT) == LINE_REFUSED); + assert(line_way(code_mark(true, false, true), false, LINE_FROM_EDIT) == LINE_KEPT); + } + + test "the product is one token: our S3 AI pipeline names no house" { + assert(MARK_ROW_S3_SPACE_EN == "our S3 AI pipeline"); + assert(HOUSE_PRODUCT == "S3AI"); + assert(PRODUCT_ONE_TOKEN); + assert(!product_mark(code_mark(false, false, false), true)); + assert(product_mark(code_mark(false, false, false), false)); + assert(product_mark(code_mark(false, false, true), false)); + assert(!product_mark(code_mark(true, false, false), false)); + assert(line_way(product_mark(code_mark(false, false, false), true), false, LINE_FROM_EDIT) == LINE_KEPT); + assert(line_way(product_mark(code_mark(false, false, false), false), false, LINE_FROM_EDIT) == LINE_REFUSED); + } + + test "Trinity College stays a house name: a refused line is rewritten, a missed one is on her reel" { + assert(MARK_ROW_COLLEGE_EN == "I studied at Trinity College"); + assert(NAME_IN_ANY_CONTEXT); + assert(name_mark(false, true)); + // The agent's line: refused before anything is spent, and written again. + assert(line_way(name_mark(false, true), false, LINE_FROM_EDIT) == LINE_REFUSED); + assert(!charged(line_way(name_mark(false, true), false, LINE_FROM_EDIT) == LINE_REFUSED)); + // Her own typed line: hers to say, college and all. + assert(line_way(name_mark(false, true), false, line_from(own_words(true, false, true))) == LINE_KEPT); + } + + test "no rule that clears a false positive clears a mark: Trinity, Trinity S3AI, T27 and @t27ai_bot stay" { + assert(BOT_SPOKEN_AS == "Trinity"); + assert(HOUSE_BRAND == "T27"); + assert(TEMPLATE_BUTTON == "@t27ai_bot"); + assert(MARK_ROW_TITLE_CASE_EN == "Write To Trinity AI"); + // Trinity alone, and Trinity before any word: Trinity S3AI, Write To Trinity AI. + assert(name_mark(false, false)); + assert(name_mark(false, true)); + // TrinityBot, and the delivery bot inside a longer handle. + assert(NAME_INSIDE_WORD); + assert(name_mark(true, false)); + // S3AI after Trinity, T27 alone, and the t27 starting @t27ai_bot. + assert(product_mark(code_mark(false, false, false), false)); + assert(code_mark(false, false, false)); + assert(code_mark(false, false, true)); + assert(line_way(name_mark(false, true), false, LINE_FROM_EDIT) == LINE_REFUSED); + assert(line_way(code_mark(false, false, false), false, LINE_FROM_EDIT) == LINE_REFUSED); + } } // phi^2 + 1/phi^2 = 3 | TRINITY