diff --git a/.claude/skills/t27f/SKILL.md b/.claude/skills/t27f/SKILL.md new file mode 100644 index 0000000000..5ac07b4b35 --- /dev/null +++ b/.claude/skills/t27f/SKILL.md @@ -0,0 +1,111 @@ +--- +id: t27f +name: T27F Topic Engine +description: Turn any topic or client brief into three lenses, a 27-lesson map, one relevant Bee task and a verified artifact, with gaps shown, evidence gates and human approval for anything that leaves the machine. Use when the user says "t27f", "explain this topic in 27 steps", "turn this brief into a plan", or gives a client brief (villa, car, campaign, product) or a research/engineering topic. +--- + +# T27F Topic Engine + + +> **Only t27: everything is written in `.t27`.** (Owner hard rule 2026-10-05; budget #7371; restated in every skill per #7588.) +> - New logic starts as a `.t27` spec with `test` / `invariant` blocks. It reaches Rust, C, Zig, JS or Python only through `t27c gen-*`, and generated files are never hand-edited (L2). +> - Hand-written code in any other language is denied by the hooks and by CI: `.rs .py .ts .js .sh .zig .c .go .v .yml .toml`, Dockerfile, Makefile; the full list is in `specs/policy/own_language.t27`. +> - Existing foreign code is debt that only shrinks. Delete it, or replace it with a spec and its generated output. +> - Thin hand-written glue is capped at 40 added lines per file and 80 per PR. No label lifts the cap. Never split a PR to slip under it. +> - What gen cannot express yet is a compiler defect. File it on #5980 with a minimal `.t27` repro instead of writing the code by hand. +> - This rule overrides any step below that says to write a script or code in another language. The full text is in `AGENTS.md`, "Only t27". + +Phase 1 vertical slice of gHashTag/trinity#1677 (parent gHashTag/trinity#1648). + +**The rules live in one place: `specs/t27f/topic_engine.t27`.** This skill is the order of +operations around it. It never restates a constant, a vocabulary or a gate; it reads them. If +this text and the spec disagree, the spec wins and this text is the bug. + +## 0. Load the engine + +Read the spec through the t27 MCP server, which is already installed in Claude Code: + +- `mcp__t27__t27_spec` with `path: "specs/t27f/topic_engine.t27"` and `source: true`. +- Until the spec is published to t27.ai, the MCP answers "not found": read the file from a + gHashTag/t27 checkout instead. Say which one you used. + +From the source, take: `LENS_NAMES`, `LENS_QUESTIONS`, `STAGE_NAMES`, the four `*_WORDS` +vocabularies, the five `*_BEES` lists, `MAX_ACTIVE_BEES`, and the functions `intake_verdict`, +`map_verdict`, `initial_status`, `route`, `dispatch_allowed`, `required_evidence`, +`evidence_accepted`, `verify`, `counts_as_progress`, `needs_human_approval`, `action_allowed`, +`after_action`. Evaluate them as written; they are small and total. + +Everything in `DETERMINISTIC_STEPS` is decided by those functions, never by your judgement. +You write only what `LLM_WRITES` names. + +## 1. Intake + +Ask for, or take from the message: the topic or brief, and its sources (links, files, notes). +Apply `intake_verdict(has_topic, source_count)`: + +- `INTAKE_EMPTY`: ask for the topic and stop. +- `INTAKE_NO_SOURCES`: continue, but every lesson starts as a GAP. Say so up front. +- `INTAKE_OK`: continue. + +## 2. Route + +Count whole-word, case-insensitive hits of the brief against each `*_WORDS` list. Show the four +counts, then apply `route(content, engineering, research, sales)`. Name the domain and the Bee list +it allows. Do not override the result; if it looks wrong, the vocabulary in the spec is what needs a +PR. + +## 3. Three lenses + +Write one short paragraph per lens, answering that lens's question from `LENS_QUESTIONS`, using only +the sources. A claim with no source is not written; it becomes a gap. + +## 4. The 27-lesson map + +Print a table of 27 rows, lens-major (`step_lens`, `step_stage`): step, lens, stage, lesson title, +source, status. + +- For each lesson, `grounded` means at least one source supports it. +- Status from `initial_status(grounded, first_grounded)`. A GAP row says **GAP: needs a source on + ** and nothing else. Never invent filler to make the map look complete. +- Count grounded lessons per lens and report `map_verdict(...)`. + +## 5. One Bee task + +Pick the first OPEN lesson. Check `dispatch_allowed(active_bees)`. Choose one capability from the +domain's Bee list and write one task for it: + +- goal (one sentence), input (the lesson and its sources), output (the artifact), +- the evidence that will verify it: `required_evidence(stage, domain)`. + +In this phase the Bee is you, in this session, or a delegated Agent if the user asks for it. Never +start more than `MAX_ACTIVE_BEES`, and never one per lesson. + +## 6. Draft, then verify + +Produce the artifact as a DRAFT and label it as one. Collect the evidence (a source link, a passing +test, a review, the client's approval). Apply `verify(DRAFT, stage, domain, evidence)`. + +- Only VERIFIED or PUBLISHED counts: `counts_as_progress`. Report the progression event as + `+POINTS_PER_VERIFIED_LESSON` point, and nothing else. `REWARD_IS_MONEY` is false: never describe + points as money, tokens, payouts or value. +- `opens_next(DRAFT)` is true, so the learner may move on while the draft waits for evidence. + +## 7. Approval gate + +Before any action, check `needs_human_approval(action)` and `action_allowed(status, action, +approved)`. + +- Publish, send to a client, any payment, anything destructive: ask the user in chat and wait for a + clear yes. A yes covers that one action only. +- Publishing or sending needs a VERIFIED item. A draft is never published. + +## 8. Keep the three layers apart + +Keep drafts, verified results and published items under separate headings (`layer_of`). If the +journey is saved, save it as a `.t27` data module, not JSON. Saving follows the user's instruction. + +## What this skill does not claim + +No runtime dispatches Bees from the spec yet, and nothing publishes on its own. Those are later +phases of gHashTag/trinity#1677. If a step needs one of them, say that it is missing; do not +simulate it. diff --git a/specs/OWNERS.md b/specs/OWNERS.md index 5af45700ab..2cbe2afd95 100644 --- a/specs/OWNERS.md +++ b/specs/OWNERS.md @@ -28,6 +28,7 @@ | `docs/` | **Z-Zeta** (with **T-Queen** for `chapters/queen.t27`) | The system documentation of `t27.ai/#/docs` as `.t27` specs (`docs/system.t27`, `KIND = "docs"`; `docs/chapters/.t27`, `KIND = "docs-chapter"`): chapter ids, sections, sources, the figure and table the site generates; English prose in `docs/system/.md`, Russian via `i18n/docs-ru.t27` | | `ui/` | **T-Queen** | The site's viewport contract as a `.t27` spec (`ui/viewport.t27`, `KIND = "viewport"`): tiers by CSS viewport width, panes per tier, the 44 px touch minimum, the six-size QA matrix and the Queen rail capacity read from `Queen.css`; the site (gHashTag/trinity) generates `viewport.generated.ts` / `.css` from a vendored copy and evaluates the spec's own test blocks; `ui/queen_evidence.t27` (`module ui_queen_evidence`): the evidence constants of the Queen's FFN and Ternary Check panel, from which the site generates `queenEvidence.generated.ts`; canonical here since S10, byte-identical to the copy the site carried alone | | `catalog/` | **T-Queen** | How the site's spec catalog and the Queen map find the repositories that write `.t27` (`catalog/discovery.t27`, `KIND = "discovery"`): the GitHub owners scanned, the exclusions, what makes a candidate qualify (a declaration in the compiler's AST, not a file extension) and the limits; the site (gHashTag/trinity) vendors a byte-identical copy and `scripts/discover-t27-worlds.mjs` reads it through the compiler wasm before touching GitHub | +| `t27f/` | **T-Queen** | The T27F topic-to-value engine of gHashTag/trinity#1677 (`t27f/topic_engine.t27`, module `T27fTopicEngine`): the 3 x 9 = 27 lesson shape, intake and gap rules, keyword routing to one domain, the Bee dispatch cap, the evidence and approval gates, and progression that is never money; driven by the Claude Code skill `.claude/skills/t27f/SKILL.md` through the t27 MCP server | | `trinity/` | **T-Queen** + **A-Architect** | The project manifest of gHashTag/trinity as `.t27` specs (`trinity/project.t27`, `KIND = "project"`; `trinity/capabilities/.t27`, `KIND = "capability"`): the pinned consumer revision, profiles, dispositions and evidence tags, dialect and build counts, dependency pins and the work packages of gHashTag/trinity#988; each card names its owner, canonical spec, implementation and generated paths, build targets, acceptance command and evidence; `tools/trinity_manifest.py` derives the inventory from a clean pinned checkout and holds the cards to it; `trinity/compiler_matrix.t27` (`KIND = "compiler-matrix"`) names the pinned compiler, the backends, the stages and the features of the executable subset with their fixtures under `bootstrap/tests/fixtures/trinity_matrix/`, and `tools/trinity_compiler_matrix.py` measures them; `trinity/build_graph.t27` (`KIND = "build-graph"`) declares the consumer's toolchain, pins, profiles, generated-file boundaries and receipt policy, and `tools/trinity_build_receipt.py` derives the graph, writes the receipts and judges drift; the consumer vendors a byte-identical copy and generates its manifest from it (S12). Shared contracts stay in their own subtrees (`numeric/`, `vsa/`, `queen/`, `tools/`, `docs/`, `ui/`) and are cited, never copied | Each subtree with substantial churn should keep a local **`OWNERS.md`** (see below). diff --git a/specs/skills/t27-t27f.t27 b/specs/skills/t27-t27f.t27 new file mode 100644 index 0000000000..48572342e3 --- /dev/null +++ b/specs/skills/t27-t27f.t27 @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/skills/t27-t27f.t27 -- skill t27/t27f +; Source of truth for the skill card on t27.ai (#/skills?skill=t27/t27f). +; The skill body lives in gHashTag/t27 at .claude/skills/t27f/SKILL.md; its rules live in +; specs/t27f/topic_engine.t27 (gHashTag/trinity#1677). +; The JSON the site serves is generated from this file by the real compiler; +; nothing below is parsed with a regex. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module skill_t27_t27f; + +pub const KIND : str = "skill"; +pub const ID : str = "t27/t27f"; +pub const NAME : str = "T27F Topic Engine"; +pub const REPO : str = "t27"; +pub const SOURCE : str = "SKILL.md"; +pub const SUMMARY_EN : str = "Turn a topic or client brief into three lenses, a 27-lesson map, one Bee task and a verified artifact"; +; The frontmatter id is the slash command. +pub const COMMAND : str = "/t27f"; +; Specs this skill stands on. +pub const SPECS : [1]str = ["specs/t27f/topic_engine.t27"]; +pub const TAGS : [3]str = ["t27f", "learning", "bees"]; +pub const ENABLED : bool = true; +; Default budget; SKILL.md declares no timeout of its own. +pub const TIMEOUT_MIN : u16 = 30; + +test basic_constants { + assert KIND == "skill"; + assert ID == "t27/t27f"; + assert NAME == "T27F Topic Engine"; + assert REPO == "t27"; + assert COMMAND == "/t27f"; + assert ENABLED == true; + assert TIMEOUT_MIN == 30; + assert TAGS[0] == "t27f"; + assert TAGS[1] == "learning"; + assert TAGS[2] == "bees"; + assert SPECS.len == 1; + assert SPECS[0] == "specs/t27f/topic_engine.t27"; +} diff --git a/specs/t27f/topic_engine.t27 b/specs/t27f/topic_engine.t27 new file mode 100644 index 0000000000..3e0a85f0ca --- /dev/null +++ b/specs/t27f/topic_engine.t27 @@ -0,0 +1,445 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/t27f/topic_engine.t27 -- the T27F topic-to-value engine, phase 1 vertical slice +; (gHashTag/trinity#1677, parent epic gHashTag/trinity#1648). +; A topic or a client brief goes in; out come three lenses, a map of 27 lessons, one relevant Bee +; task, an artifact that is verified before it counts, and a progression event. The same functions +; serve every domain: a villa sale and a GF16 research note walk the same 27 steps. +; What this file decides, deterministically and without a language model: +; - the shape: 3 lenses x 9 steps = 27, each lens split LEARN / PRACTICE / DELIVER; +; - intake and grounding: a lesson with no source is a GAP, shown as a gap, never filled in; +; - routing: which domain a brief belongs to, from keyword hits the host counts; +; - dispatch: at most MAX_ACTIVE_BEES Bees per journey, never 27 always-on agents; +; - the evidence gate: which evidence turns a DRAFT into VERIFIED, per stage and domain; +; - progression: only VERIFIED or PUBLISHED lessons count, and the reward is never money; +; - the approval gate: publish, send to client, payment and destructive actions need a human yes. +; What the language model writes, and this file never judges: the lens text, lesson titles, the +; Bee task text and the artifact draft itself (LLM_WRITES below). +; How it is used today: the Claude Code skill `.claude/skills/t27f/SKILL.md` reads this file through +; the t27 MCP server (`t27_spec path=specs/t27f/topic_engine.t27 source=true`, once published to +; t27.ai; the local file until then) and applies these functions step by step. +; Not claimed here: no runtime dispatches Bees from this file yet, nothing publishes on its own, and +; no host imports a generated binding yet. Those are later phases of gHashTag/trinity#1677. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module T27fTopicEngine; + +/// Shape of every journey, whatever the topic. +pub const LENS_COUNT : u8 = 3; +pub const STEPS_PER_LENS : u8 = 9; +pub const STAGE_COUNT : u8 = 3; +pub const STEPS_PER_STAGE : u8 = 3; +pub const LESSON_COUNT : u8 = 27; + +/// Lenses, in map order. A step's lens is step / STEPS_PER_LENS. +pub const LENS_STRUCTURE : u8 = 0; +pub const LENS_PROCESS : u8 = 1; +pub const LENS_VALUE : u8 = 2; +pub const LENS_NAMES : str = "structure,process,value"; +/// What each lens asks of a topic, for the model that writes the lens text. +pub const LENS_QUESTIONS : str = "what is it made of,how does it work and change,what is it worth and to whom"; + +/// Stages inside one lens. A step's stage is (step % STEPS_PER_LENS) / STEPS_PER_STAGE. +pub const STAGE_LEARN : u8 = 0; +pub const STAGE_PRACTICE : u8 = 1; +pub const STAGE_DELIVER : u8 = 2; +pub const STAGE_NAMES : str = "learn,practice,deliver"; + +/// Intake verdicts. +pub const INTAKE_EMPTY : u8 = 0; +pub const INTAKE_NO_SOURCES : u8 = 1; +pub const INTAKE_OK : u8 = 2; + +/// Map verdicts, from the number of grounded lessons per lens. +pub const MAP_NEED_SOURCES : u8 = 0; +pub const MAP_PARTIAL : u8 = 1; +pub const MAP_COMPLETE : u8 = 2; + +/// Domains. GENERAL is what a brief with no keyword hit gets. +pub const DOMAIN_GENERAL : u8 = 0; +pub const DOMAIN_CONTENT : u8 = 1; +pub const DOMAIN_ENGINEERING : u8 = 2; +pub const DOMAIN_RESEARCH : u8 = 3; +pub const DOMAIN_SALES : u8 = 4; +pub const DOMAIN_NAMES : str = "general,content,engineering,research,sales"; + +/// Keyword vocabularies, comma separated. The host counts how many words of the brief appear in each +/// list (whole words, case-insensitive) and passes the four counts to route(). +pub const CONTENT_WORDS : str = "blog,post,campaign,reel,video,channel,audience,content,newsletter"; +pub const ENGINEERING_WORDS : str = "fpga,rtl,verilog,compiler,spec,code,bug,test,firmware,api"; +pub const RESEARCH_WORDS : str = "paper,research,experiment,dataset,proof,preprint,result,benchmark,gf16"; +pub const SALES_WORDS : str = "villa,property,listing,car,vehicle,client,lead,sale,product,price"; + +/// The Bee capabilities a domain may call on, comma separated. One Bee per step, chosen from here. +pub const GENERAL_BEES : str = "researcher,writer"; +pub const CONTENT_BEES : str = "researcher,writer,editor"; +pub const ENGINEERING_BEES : str = "researcher,coder,tester"; +pub const RESEARCH_BEES : str = "researcher,analyst,reviewer"; +pub const SALES_BEES : str = "researcher,copywriter,listing-builder"; + +/// Dispatch cap: relevant Bees only, never one agent per lesson. +pub const MAX_ACTIVE_BEES : u8 = 3; +pub const BEES_PER_STEP : u8 = 1; + +/// Evidence kinds a Bee or a person can attach to a draft. +pub const EVIDENCE_NONE : u8 = 0; +pub const EVIDENCE_SOURCE_LINK : u8 = 1; +pub const EVIDENCE_TEST_PASS : u8 = 2; +pub const EVIDENCE_REVIEW : u8 = 3; +pub const EVIDENCE_CLIENT_APPROVAL : u8 = 4; + +/// Lesson statuses, ordered. Drafts, verified results and published items are three layers. +pub const STATUS_GAP : u8 = 0; +pub const STATUS_LOCKED : u8 = 1; +pub const STATUS_OPEN : u8 = 2; +pub const STATUS_DRAFT : u8 = 3; +pub const STATUS_VERIFIED : u8 = 4; +pub const STATUS_PUBLISHED : u8 = 5; +pub const STATUS_NAMES : str = "gap,locked,open,draft,verified,published"; + +pub const LAYER_NONE : u8 = 0; +pub const LAYER_DRAFT : u8 = 1; +pub const LAYER_VERIFIED : u8 = 2; +pub const LAYER_PUBLISHED : u8 = 3; + +/// Actions on an artifact, ordered by impact. From PUBLISH up, a human must say yes first. +pub const ACTION_DRAFT_ONLY : u8 = 0; +pub const ACTION_EXPORT_LOCAL : u8 = 1; +pub const ACTION_PUBLISH : u8 = 2; +pub const ACTION_SEND_TO_CLIENT : u8 = 3; +pub const ACTION_PAYMENT : u8 = 4; +pub const ACTION_DESTRUCTIVE : u8 = 5; + +/// Progression. Points measure verified learning; they are not money and never convert to it. +pub const POINTS_PER_VERIFIED_LESSON : u32 = 1; +pub const REWARD_IS_MONEY : bool = false; + +/// Which steps are rules and which are writing. The skill runs the first list from this file and +/// asks the model only for the second. +pub const DETERMINISTIC_STEPS : str = "intake,route,map_verdict,dispatch_cap,evidence_gate,progress,approval_gate"; +pub const LLM_WRITES : str = "lens_text,lesson_titles,bee_task_text,artifact_draft"; + +/// The lens a step belongs to. Steps are lens-major: 0..8 structure, 9..17 process, 18..26 value. +pub fn step_lens(step: u8) -> u8 { + return step / STEPS_PER_LENS; +} + +/// The stage of a step inside its lens. +pub fn step_stage(step: u8) -> u8 { + return (step % STEPS_PER_LENS) / STEPS_PER_STAGE; +} + +/// The step at a lens, stage and slot (slot 0..2). +pub fn step_index(lens: u8, stage: u8, slot: u8) -> u8 { + return lens * STEPS_PER_LENS + stage * STEPS_PER_STAGE + slot; +} + +pub fn valid_step(step: u8) -> bool { + return step < LESSON_COUNT; +} + +/// Intake: no topic stops the journey; a topic with no source still runs, every lesson as a GAP. +pub fn intake_verdict(has_topic: bool, source_count: u32) -> u8 { + if (has_topic == false) { + return INTAKE_EMPTY; + } + if (source_count == 0) { + return INTAKE_NO_SOURCES; + } + return INTAKE_OK; +} + +/// Map verdict from grounded lessons per lens (each 0..9). A lens with nothing grounded means the +/// map asks for sources before it claims anything about that lens. +pub fn map_verdict(grounded_structure: u8, grounded_process: u8, grounded_value: u8) -> u8 { + if (grounded_structure == 0 || grounded_process == 0 || grounded_value == 0) { + return MAP_NEED_SOURCES; + } + var total : u32 = (grounded_structure as u32) + (grounded_process as u32) + (grounded_value as u32); + if (total >= (LESSON_COUNT as u32)) { + return MAP_COMPLETE; + } + return MAP_PARTIAL; +} + +/// The status a lesson starts in. Only the first grounded lesson of the map is open. +pub fn initial_status(grounded: bool, first_grounded: bool) -> u8 { + if (grounded == false) { + return STATUS_GAP; + } + if (first_grounded) { + return STATUS_OPEN; + } + return STATUS_LOCKED; +} + +/// Route a brief by keyword hits. The most hits wins; on a tie the earlier domain in DOMAIN_NAMES +/// order wins; no hit at all is GENERAL. +pub fn route(content_hits: u32, engineering_hits: u32, research_hits: u32, sales_hits: u32) -> u8 { + var best : u8 = DOMAIN_GENERAL; + var best_hits : u32 = 0; + if (content_hits > best_hits) { + best = DOMAIN_CONTENT; + best_hits = content_hits; + } + if (engineering_hits > best_hits) { + best = DOMAIN_ENGINEERING; + best_hits = engineering_hits; + } + if (research_hits > best_hits) { + best = DOMAIN_RESEARCH; + best_hits = research_hits; + } + if (sales_hits > best_hits) { + best = DOMAIN_SALES; + best_hits = sales_hits; + } + return best; +} + +/// One more Bee may start only while fewer than MAX_ACTIVE_BEES are working. +pub fn dispatch_allowed(active_bees: u8) -> bool { + return active_bees < MAX_ACTIVE_BEES; +} + +/// The evidence a stage asks for first, per domain. +pub fn required_evidence(stage: u8, domain: u8) -> u8 { + if (stage == STAGE_LEARN) { + return EVIDENCE_SOURCE_LINK; + } + if (stage == STAGE_PRACTICE) { + if (domain == DOMAIN_ENGINEERING) { + return EVIDENCE_TEST_PASS; + } + return EVIDENCE_REVIEW; + } + if (domain == DOMAIN_SALES || domain == DOMAIN_CONTENT) { + return EVIDENCE_CLIENT_APPROVAL; + } + return EVIDENCE_REVIEW; +} + +/// Whether a piece of evidence clears the gate. Outside engineering, a passing test also clears +/// PRACTICE; nothing else substitutes. +pub fn evidence_accepted(stage: u8, domain: u8, evidence: u8) -> bool { + if (evidence == EVIDENCE_NONE) { + return false; + } + if (evidence == required_evidence(stage, domain)) { + return true; + } + if (stage == STAGE_PRACTICE && evidence == EVIDENCE_TEST_PASS) { + return true; + } + return false; +} + +/// A DRAFT becomes VERIFIED only with accepted evidence. Every other status stays as it is. +pub fn verify(status: u8, stage: u8, domain: u8, evidence: u8) -> u8 { + if (status == STATUS_DRAFT && evidence_accepted(stage, domain, evidence)) { + return STATUS_VERIFIED; + } + return status; +} + +/// The learner may open the next lesson once this one has a draft, so learning does not wait on a Bee. +pub fn opens_next(status: u8) -> bool { + return status >= STATUS_DRAFT && status <= STATUS_PUBLISHED; +} + +/// Progress counts only after verification. +pub fn counts_as_progress(status: u8) -> bool { + return status >= STATUS_VERIFIED && status <= STATUS_PUBLISHED; +} + +pub fn progress_points(status: u8) -> u32 { + if (counts_as_progress(status)) { + return POINTS_PER_VERIFIED_LESSON; + } + return 0; +} + +/// Which of the three layers an item lives in. +pub fn layer_of(status: u8) -> u8 { + if (status == STATUS_DRAFT) { + return LAYER_DRAFT; + } + if (status == STATUS_VERIFIED) { + return LAYER_VERIFIED; + } + if (status == STATUS_PUBLISHED) { + return LAYER_PUBLISHED; + } + return LAYER_NONE; +} + +pub fn needs_human_approval(action: u8) -> bool { + return action >= ACTION_PUBLISH; +} + +/// Whether an action may run now. High-impact actions need approval; publishing and sending to a +/// client need a VERIFIED item as well. +pub fn action_allowed(status: u8, action: u8, approved: bool) -> bool { + if (action == ACTION_DRAFT_ONLY) { + return status >= STATUS_OPEN && status <= STATUS_PUBLISHED; + } + if (action == ACTION_EXPORT_LOCAL) { + return status >= STATUS_DRAFT && status <= STATUS_PUBLISHED; + } + if (approved == false) { + return false; + } + if (action == ACTION_PUBLISH || action == ACTION_SEND_TO_CLIENT) { + return status == STATUS_VERIFIED; + } + return action <= ACTION_DESTRUCTIVE; +} + +/// The status after an action: a VERIFIED item that is published or sent becomes PUBLISHED. +pub fn after_action(status: u8, action: u8, approved: bool) -> u8 { + if (action_allowed(status, action, approved)) { + if (action == ACTION_PUBLISH || action == ACTION_SEND_TO_CLIENT) { + return STATUS_PUBLISHED; + } + } + return status; +} + +test shape_is_three_lenses_of_nine { + assert LENS_COUNT * STEPS_PER_LENS == LESSON_COUNT; + assert STAGE_COUNT * STEPS_PER_STAGE == STEPS_PER_LENS; + assert step_lens(0) == LENS_STRUCTURE; + assert step_lens(8) == LENS_STRUCTURE; + assert step_lens(9) == LENS_PROCESS; + assert step_lens(26) == LENS_VALUE; + assert step_stage(0) == STAGE_LEARN; + assert step_stage(4) == STAGE_PRACTICE; + assert step_stage(8) == STAGE_DELIVER; + assert step_stage(18) == STAGE_LEARN; + assert step_index(LENS_VALUE, STAGE_DELIVER, 2) == 26; + assert step_index(LENS_PROCESS, STAGE_PRACTICE, 1) == 13; + assert valid_step(26); + assert valid_step(27) == false; +} + +test gaps_are_shown_not_filled { + assert intake_verdict(false, 5) == INTAKE_EMPTY; + assert intake_verdict(true, 0) == INTAKE_NO_SOURCES; + assert intake_verdict(true, 2) == INTAKE_OK; + assert map_verdict(9, 0, 9) == MAP_NEED_SOURCES; + assert map_verdict(3, 4, 5) == MAP_PARTIAL; + assert map_verdict(9, 9, 9) == MAP_COMPLETE; + assert initial_status(false, true) == STATUS_GAP; + assert initial_status(true, true) == STATUS_OPEN; + assert initial_status(true, false) == STATUS_LOCKED; + // a gap is not a draft, so it cannot be verified into progress + assert verify(STATUS_GAP, STAGE_LEARN, DOMAIN_GENERAL, EVIDENCE_SOURCE_LINK) == STATUS_GAP; + assert counts_as_progress(STATUS_GAP) == false; +} + +test routing_picks_one_domain { + assert route(0, 0, 0, 0) == DOMAIN_GENERAL; + assert route(2, 0, 0, 0) == DOMAIN_CONTENT; + assert route(0, 3, 1, 0) == DOMAIN_ENGINEERING; + // a tie goes to the earlier domain + assert route(2, 0, 0, 2) == DOMAIN_CONTENT; + assert route(0, 0, 2, 2) == DOMAIN_RESEARCH; +} + +test dispatch_is_capped { + assert dispatch_allowed(0); + assert dispatch_allowed(2); + assert dispatch_allowed(MAX_ACTIVE_BEES) == false; + assert dispatch_allowed(LESSON_COUNT) == false; +} + +test demo_villa_sale_brief { + // "List a sea-view villa for a client and draft the sale post": villa, client, sale, listing -> 4 + // sales hits; post -> 1 content hit + var domain : u8 = route(1, 0, 0, 4); + assert domain == DOMAIN_SALES; + assert required_evidence(STAGE_LEARN, domain) == EVIDENCE_SOURCE_LINK; + assert required_evidence(STAGE_PRACTICE, domain) == EVIDENCE_REVIEW; + assert required_evidence(STAGE_DELIVER, domain) == EVIDENCE_CLIENT_APPROVAL; + // the listing draft is reviewed, but delivery needs the client's yes + assert verify(STATUS_DRAFT, STAGE_DELIVER, domain, EVIDENCE_REVIEW) == STATUS_DRAFT; + assert verify(STATUS_DRAFT, STAGE_DELIVER, domain, EVIDENCE_CLIENT_APPROVAL) == STATUS_VERIFIED; + // sending to the client still needs a human approval at send time + assert action_allowed(STATUS_VERIFIED, ACTION_SEND_TO_CLIENT, false) == false; + assert after_action(STATUS_VERIFIED, ACTION_SEND_TO_CLIENT, true) == STATUS_PUBLISHED; +} + +test demo_gf16_research_topic { + // "How accurate is GF16 against bf16 on a benchmark": gf16, benchmark -> 2 research hits + var domain : u8 = route(0, 0, 2, 0); + assert domain == DOMAIN_RESEARCH; + assert required_evidence(STAGE_PRACTICE, domain) == EVIDENCE_REVIEW; + assert evidence_accepted(STAGE_PRACTICE, domain, EVIDENCE_TEST_PASS); + assert required_evidence(STAGE_DELIVER, domain) == EVIDENCE_REVIEW; + assert evidence_accepted(STAGE_DELIVER, domain, EVIDENCE_CLIENT_APPROVAL) == false; + assert verify(STATUS_DRAFT, STAGE_LEARN, domain, EVIDENCE_NONE) == STATUS_DRAFT; + assert verify(STATUS_DRAFT, STAGE_LEARN, domain, EVIDENCE_SOURCE_LINK) == STATUS_VERIFIED; +} + +test engineering_practice_needs_a_test { + assert required_evidence(STAGE_PRACTICE, DOMAIN_ENGINEERING) == EVIDENCE_TEST_PASS; + assert evidence_accepted(STAGE_PRACTICE, DOMAIN_ENGINEERING, EVIDENCE_REVIEW) == false; + assert evidence_accepted(STAGE_PRACTICE, DOMAIN_ENGINEERING, EVIDENCE_TEST_PASS); +} + +test progress_only_after_verification { + assert opens_next(STATUS_OPEN) == false; + assert opens_next(STATUS_DRAFT); + assert counts_as_progress(STATUS_DRAFT) == false; + assert counts_as_progress(STATUS_VERIFIED); + assert counts_as_progress(STATUS_PUBLISHED); + assert progress_points(STATUS_DRAFT) == 0; + assert progress_points(STATUS_VERIFIED) == POINTS_PER_VERIFIED_LESSON; +} + +test layers_stay_separate { + assert layer_of(STATUS_OPEN) == LAYER_NONE; + assert layer_of(STATUS_DRAFT) == LAYER_DRAFT; + assert layer_of(STATUS_VERIFIED) == LAYER_VERIFIED; + assert layer_of(STATUS_PUBLISHED) == LAYER_PUBLISHED; +} + +test high_impact_actions_need_approval { + assert needs_human_approval(ACTION_DRAFT_ONLY) == false; + assert needs_human_approval(ACTION_EXPORT_LOCAL) == false; + assert needs_human_approval(ACTION_PUBLISH); + assert needs_human_approval(ACTION_SEND_TO_CLIENT); + assert needs_human_approval(ACTION_PAYMENT); + assert needs_human_approval(ACTION_DESTRUCTIVE); + assert action_allowed(STATUS_OPEN, ACTION_DRAFT_ONLY, false); + assert action_allowed(STATUS_DRAFT, ACTION_EXPORT_LOCAL, false); + assert action_allowed(STATUS_VERIFIED, ACTION_PUBLISH, false) == false; + assert action_allowed(STATUS_DRAFT, ACTION_PUBLISH, true) == false; + assert action_allowed(STATUS_VERIFIED, ACTION_PUBLISH, true); + assert action_allowed(STATUS_VERIFIED, ACTION_PAYMENT, false) == false; + assert action_allowed(STATUS_VERIFIED, ACTION_DESTRUCTIVE, false) == false; + assert after_action(STATUS_DRAFT, ACTION_PUBLISH, true) == STATUS_DRAFT; + assert after_action(STATUS_VERIFIED, ACTION_PUBLISH, false) == STATUS_VERIFIED; +} + +invariant reward_is_not_money { + assert REWARD_IS_MONEY == false; +} + +invariant statuses_are_ordered { + assert STATUS_GAP < STATUS_LOCKED; + assert STATUS_LOCKED < STATUS_OPEN; + assert STATUS_OPEN < STATUS_DRAFT; + assert STATUS_DRAFT < STATUS_VERIFIED; + assert STATUS_VERIFIED < STATUS_PUBLISHED; +} + +invariant few_bees_not_twenty_seven { + assert MAX_ACTIVE_BEES < LESSON_COUNT; + assert BEES_PER_STEP == 1; +} + +invariant approval_starts_at_publish { + assert ACTION_EXPORT_LOCAL < ACTION_PUBLISH; + assert ACTION_PUBLISH < ACTION_SEND_TO_CLIENT; + assert ACTION_PAYMENT < ACTION_DESTRUCTIVE; +}