SLOPSHOPPER

asd-ste100

ASD-STE100 (Simplified Technical English) and 80% Karpathy Mode plugin, mod, and skill for Claude Code. Enforces clean, unambiguous technical explanations.

newpanespinnerguardcommandtoast
★ 2v2026.10.2+20MITupdated 2026-10-02JAICHANGPARK/ASD-STE100
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · asd-ste100
│ ┃ STE ✕ › fix the failing auth test and add an audit log call │ ┃ ┌───────────────────────────────────────┬─── │ ┃ │ ASD-STE100 STE MAINTENANCE MANUAL │ TA ⏺ Read(src/auth.ts) │ ┃ │ LAST ANSWER │ PA ⎿ Read 6 lines │ ┃ ├───────────────────────────────────────┴─── ⏺ Update(src/auth.ts) │ ┃ │ DESCRIPTION & OPERATION ⎿ Added 2 lines, removed 1 line │ ┃ ├───────────────────┬────────────┬──────┬─── ⏺ Bash(bun test) │ ┃ │ EFFECTIVITY ALL │ MODE 80% … │ REV 0 │ D ⎿ 3 pass, 1 fail │ ┃ └───────────────────┴────────────┴──────┴─── │ ┃ Rewriting the answer as an ASD-STE100 docume ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ │ ┃ Press o to read the original answer now. ✻ Worked for 42s · done 4:20 PM │ ┃ │ ┃ ┌───────────────────┬─────────────┬───────── › /asd │ ┃ │ STE100 ISSUE 9 │ SCORE 97/1… │ 00-01-01 ⎿ asd-ste100: [ASD-STE100 Simplified Technical English Assistant] │ ┃ └───────────────────┴─────────────┴───────── ⎿ asd-ste100: Inspired by Andrej Karpathy's controlled language gu │ ┃ v: view o: original c: check s: session ⎿ asd-ste100: │ ⎿ asd-ste100: Commands: │ ⎿ asd-ste100: /asd on - Activate automatic STE promp │ ⎿ asd-ste100: /asd off - Deactivate STE enhancement │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · STE
┌───────────────────────────────────────┬──────────────┐ │ ASD-STE100 STE MAINTENANCE MANUAL │ TASK 00-01-… │ │ LAST ANSWER │ PAGE BLOCK … │ ├───────────────────────────────────────┴──────────────┤ │ DESCRIPTION & OPERATION │ ├───────────────────┬────────────┬──────┬──────────────┤ │ EFFECTIVITY ALL │ MODE 80% … │ REV 0 │ DATE 2025-1…… └───────────────────┴────────────┴──────┴──────────────┘ Rewriting the answer as an ASD-STE100 document… Press o to read the original answer now. ┌───────────────────┬─────────────┬────────────────────┐ │ STE100 ISSUE 9 │ SCORE 97/1… │ 00-01-01 PAGE 001 │ └───────────────────┴─────────────┴────────────────────┘ v: view o: original c: check s: session r: rewrite x: c
README

ASD-STE100: Simplified Technical English & 80% Karpathy Mode

Claude Code Plugin · Mod · Agent Skill · Linter & CLI Controlled technical English specification to minimize cognitive load, eliminate ambiguity, and optimize LLM reasoning and human oversight.

License: MIT Claude Code Specification


📌 Background & Motivation

As AI coding and autonomous agents perform more legwork, human engineers spend significantly more time reviewing, understanding, and validating LLM outputs.

Former OpenAI Chief Scientist and Tesla AI Director Andrej Karpathy highlighted this paradigm shift:

"Writing. Something I've had success with: Ask your LLM to explain something in ASD-STE100, it's a controlled language specification originally developed for aerospace maintenance documentation. LLMs well-versed in this language and it comes with heavy constraints on clean writing style that I often find a lot more readable. Sometimes I've tried to soften it a bit e.g. ask for '80% of the way to ASD-STE100' because the spec is quite stringent...

In summary:

  • As LLMs get better, they will do more and more of the legwork autonomously, and a lot more of our work will rise up the abstractions into oversight and understanding.
  • Luckily, LLMs can help here too because as intelligence and code are increasingly abundant, you can ask for large, custom, discardable software artifacts (e.g. web apps, video explainers) that would have never made sense to create before."

— Andrej Karpathy on X

This repository packages ASD-STE100 into a complete ecosystem:

  1. Claude Code Mod (hooks/register.js): An internal Claude Code extension that adds /asd commands, UI status indicators, the STE pane, and STE rules in the system prompt.
  2. Claude Code Plugin (.claude-plugin/plugin.json): Installable plugin compliant with the new Claude Code plugin architecture.
  3. Agent Skill (skills/asd-ste100/SKILL.md): A rich skill loaded by Claude Code, Google Antigravity, and other coding assistants.
  4. Core Rule Engine & Linter (lib/ste-engine.js): Checks sentence and paragraph length, passive voice (with the Issue 9 descriptive-text exception), verb forms, multi-word nouns, unapproved words, phrasal verbs, semicolons, contractions, and Latin abbreviations. It uses lightweight heuristics (no part-of-speech tagging) and a curated word list, not the official ~900-word dictionary.
  5. Standalone CLI (bin/ste.js): Lint text files or format system prompts directly from your terminal.

🚀 Quick Start

1. Install as a Claude Code Plugin

Requirements: Claude Code v2.1.287 or later for the mod (/asd command, tools, STE pane, system prompt rules, spinner indicator). Run claude --version to check. The skill works on any version that supports plugins.

This repository is its own plugin marketplace. One plugin install gives you both the skill and the mod (the /asd command and the STE pane). Install it from GitHub:

claude plugin marketplace add JAICHANGPARK/ASD-STE100
claude plugin install asd-ste100@asd-ste100

Or, inside a Claude Code session:

/plugin marketplace add JAICHANGPARK/ASD-STE100
/plugin install asd-ste100@asd-ste100
/reload-plugins

To try it for one session without installing (for example, from a local clone):

claude --plugin-dir /path/to/ASD-STE100

To update later, run claude plugin update asd-ste100@asd-ste100.

Older Claude Code (v2.1.286 and earlier): mods were early access. The skill loads, but the mod prints hooks module not loaded and /asd does not exist. Update Claude Code, or start it with CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1. Remove that variable after you update.

To see what the mod hooks and calls before you install it, run claude plugin validate . in a clone.

2. Use the /asd Command (Mod)

Type / to see the /asd command:

/asd on             # Activate automatic STE prompt enhancement
/asd 80             # Use Karpathy's 80% Pragmatic Mode (recommended); changes only the mode
/asd strict         # Use 100% Strict ASD-STE100 Mode; changes only the mode
/asd pane           # Show the last answer as an STE document in a side pane
/asd auto [on|off]  # Rewrite each answer as an STE document in the pane (default on)
/asd session [lite] # Write an STE record of the whole session in the pane (on request only)
/asd check <text>   # Lint and score a sentence or paragraph
/asd rewrite <text> # Rewrite any text into clean STE format
/asd status         # Display active mode and status
/asd off            # Deactivate STE mode

The mod keeps Claude's answer as Claude wrote it. After each answer, the STE pane opens and shows that answer converted into an ASD-STE100 document. You do not need /asd on for the pane. Use /asd on only when you want Claude to write the answer itself in STE (the same effect as the skill). Use /asd off to go back.

/asd 80 and /asd strict change only the mode that the pane and STE mode use. Only /asd on makes the answer itself STE. /asd on, /asd 80, /asd strict, and /asd off save the state machine-wide. The saved state applies to every project and overrides the defaultMode and autoInject plugin options.

STE session record

/asd session writes a record of the whole Claude Code session as an ASD-STE100 document, in the STE pane (key s). The record has the sections Purpose, Decisions, Completed work (numbered steps in time order), and Open items, in the language of the conversation.

  • /asd session asks the session's own model once, over the conversation that it already has in its prompt cache. This gives the most accurate record.
  • /asd session lite sends the conversation text to a small model (haiku). It costs less. The mod also uses it when the full way fails.
  • The record uses tokens, so the mod makes it only when you ask: with /asd session, or with g on the pane's session tab. It never runs by itself.

STE pane

After each answer, the STE pane opens beside the transcript. A small model (haiku) rewrites the answer as an ASD-STE100 document, and the pane shows that STE version as a page of a maintenance manual:

┌───────────────────────────────────────┬──────────────────┐
│ ASD-STE100  STE MAINTENANCE MANUAL    │ TASK 00-01-03    │
│ FLUTTER WIDGETS                       │ PAGE BLOCK 201   │
├───────────────────────────────────────┴──────────────────┤
│ MAINTENANCE PRACTICES                                    │
├───────────────────┬─────────────┬────────┬───────────────┤
│ EFFECTIVITY ALL   │ MODE 80% STE│ REV 1  │ DATE 2026-10-02│
└───────────────────┴─────────────┴────────┴───────────────┘
STE version

 1. GENERAL
    A. In Flutter, everything on the screen is a widget.
 2. PROCEDURE
    (1) Run the app in debug mode.
 ┌──────────────── WARNING ────────────────┐
 │ Do not put a large tree in one build(). │
 └─────────────────────────────────────────┘

Original → STE: avg words 19 → 7 · too long 3 → 0
┌──────────────────┬──────────────────┬────────────────────┐
│ ASD-STE100 ISSUE 9│ SCORE 97/100     │ 00-01-03  PAGE 201 │
└──────────────────┴──────────────────┴────────────────────┘
  • A header block, as in an aircraft maintenance manual: the task number, the title, the page block (001 description and operation, 201 maintenance practices when the text has steps), effectivity, mode, revision and date.
  • Sections numbered 1., sentences A., procedure steps (1), list items (a). One idea in each sentence.
  • WARNING, CAUTION, and NOTE as boxes with the signal word in the middle.
  • Code blocks drawn as markdown, as in an assistant reply. Tables drawn as grids that fit the pane, with long cells wrapped.
  • Unapproved words in red, with the approved word after them (utilize →USE).
  • A footer block with the standard, the STE check score and the page. The change summary sits above it. Each r adds one to the revision.
  • The STE version uses the language of the answer. A Korean answer becomes a Korean STE document: the mod applies the STE rules (short sentences, one idea in each sentence, the active voice, simple words) to that language.

The header shows the STE check score of the text in view, and one line that says what the rewrite changed, for example Original → STE: avg words 19 → 7 · too long 3 → 0 · tables 1 → 0 · phrasal verbs 2 → 0. Pane keys: v STE version, o original answer, c check (score and findings), r rewrite again, x close.

The rewrite costs one small model call for each answer. Use /asd auto off to stop it, and press r when you want a rewrite. The pane docks beside the transcript in the fullscreen layout. Claude Code shows a pane that opens by itself only when the terminal is 144 columns or wider; in a narrower terminal, type /asd pane. Use /asd pane to open it, and /asd pane off to stop it from opening by itself.

The mod also gives Claude two tools: mcp__asd-ste100__validate_ste and mcp__asd-ste100__rewrite_ste.

When active, Claude Code displays a live indicator beside the spinner:

Thinking · STE [PRAGMATIC 80%]…

The indicator appears in the terminal and in the Desktop app. In claude -p and the VS Code chat panel, the hooks run, but nothing is drawn.


3. Run as a Standalone Terminal CLI

# Lint a piece of text:
./bin/ste.js check "You should utilize this script prior to commencing the process."

# Output:
# ========================================
#  ASD-STE100 Compliance Report (PRAGMATIC MODE)
# ========================================
# Score: 68 / 100
# Issues:
#  - [UNAPPROVED_WORD] "prior to" is unapproved in ASD-STE100. Use "before" instead.
#  - [UNAPPROVED_WORD] "should" is unapproved in ASD-STE100. Use "must (or explain optional choice)" instead.
#  - [UNAPPROVED_WORD] "utilize" is unapproved in ASD-STE100. Use "use" instead.
#  - [UNAPPROVED_WORD] "commencing" is unapproved in ASD-STE100. Use "starting" instead.

# Generate system prompt instructions for any LLM:
./bin/ste.js prompt --diagram

4. Use as an Agent Skill in Antigravity or Claude

The skill file is located at: skills/asd-ste100/SKILL.md

After you install the plugin, Claude Code loads the skill as asd-ste100:asd-ste100. Claude uses it automatically when a request matches, or you can call it with /asd-ste100:asd-ste100.

Whenever you want an agent to explain or rewrite technical logic, simply ask:

"Explain the architecture of our authentication service in 80% ASD-STE100 with a Mermaid diagram."


⚖️ Strict 100% vs. Karpathy 80% Pragmatic Mode

Strict mode follows ASD-STE100 Issue 9 (January 2025): 53 writing rules in 9 sections and a dictionary of about 900 approved words. The official standard is free on request at asd-ste100.org. This repository summarizes the rules and does not include the dictionary.

Rule DimensionStrict Mode (100% ASD-STE100)80% Pragmatic Mode (Karpathy) [Default]
Primary Use CaseAerospace, defense, ISO hardware manualsSoftware engineering, AI systems, architecture review
Sentence LengthInstructions: ≤ 20 words<br>Descriptions: ≤ 25 wordsTarget 15–22 words (hard cap at 25 words)
VoiceActive voice. Passive only in descriptive text when the agent is unknown. Imperative for steps.Active voice with direct subject-verb-object clarity
VocabularyApproved ASD-STE100 dictionary words, plus technical nouns and technical verbsReplaces bureaucratic jargon with simple verbs (use, start, stop, before), but allows modern tech nouns (API, cache, Docker)
Noun ClustersMax 3 consecutive nounsMax 3 consecutive nouns (separated by prepositions)
Modal VerbsReplace shall, should, could, might with must, can, or an exact conditionDirect directives (must, can, or specific conditional rules)
PunctuationNo semicolons, contractions, or Latin abbreviations (e.g., i.e., etc.)Avoid semicolons
ParagraphsOne topic, max 6 sentencesOne topic, max 6 sentences
SafetyWARNING / CAUTION, a command first, then the riskSame, for data loss, security risks, and outages
Cognitive ModalityStandard technical text and tablesPairs text with Mermaid diagrams or Interactive HTML

🎨 The 4 Cognitive Modalities (Karpathy Framework)

Andrej Karpathy emphasized expanding beyond plain text into high-abstraction artifacts:

  1. Writing (ASD-STE100): Clean, high-speed procedural and descriptive English.
  2. Diagrams (Mermaid): Structural flowcharts and sequence diagrams embedded directly in markdown.
  3. Web Pages (Interactive HTML): Self-contained, responsive HTML/JS widgets for interactive inspection. (See examples/interactive_explainer.html)
  4. Explainer Videos (3b1b / Manim): Programmatic 3Blue1Brown-style vector animations with narration cues for Text-to-Speech engines like ElevenLabs. (See examples/manim_3b1b_explainer.py)

📁 Repository Structure

ASD-STE100/
├── .claude-plugin/
│   ├── plugin.json            # Claude Code Plugin Manifest
│   └── marketplace.json       # Marketplace entry (install from GitHub)
├── hooks/
│   ├── hooks.json             # Mod Hook configuration
│   └── register.js            # Claude Code Mod implementation (/asd command, UI, tools)
├── skills/
│   └── asd-ste100/
│       └── SKILL.md           # Authoritative Agent Skill documentation
├── lib/
│   └── ste-engine.js          # Core ASD-STE100 rule engine & validator
├── bin/
│   └── ste.js                 # Terminal CLI utility
├── test/
│   └── ste.test.js            # Automated unit test suite
├── examples/
│   ├── before_and_after.md    # Real-world before/after technical transformations
│   ├── interactive_explainer.html # Interactive browser-based STE explainer
│   └── manim_3b1b_explainer.py   # 3b1b / Manim video explainer script
├── package.json
└── README.md

🧪 Testing

Run the automated test suite:

npm test

📝 Changelog

See CHANGELOG.md.


📄 License

MIT License. Open source and free for personal and commercial use.

Source 6 files
hooks/register.js 501 lines
1/**
2 * ASD-STE100 Claude Code Mod
3 * 
4 * Provides:
5 * - /asd command (on, off, 80, strict, check, rewrite, status)
6 * - Automatic prompt guidance injection when active
7 * - UI status indicator & spinner suffix
8 * - STE pane: the last answer as an ASD-STE100 document beside the transcript (/asd pane)
9 * - Built-in Claude tools: mcp__asd-ste100__validate_ste & mcp__asd-ste100__rewrite_ste
10 */
11
12import { atom, read, update } from 'claude-code';
13import { validateText, buildSystemPrompt } from '../lib/ste-engine.js';
14import { PANE_ID, drawPane } from './pane.jsx';
15import { proseOf } from './sheet.jsx';
16import { STE_REQUEST } from './view.jsx';
17
18let steActive = false;
19let steMode = 'pragmatic'; // 'pragmatic' (80% Karpathy mode) or 'strict' (100% ASD-STE100)
20let paneAuto = true; // Open the STE pane by itself after each answer
21let autoRewrite = true; // Rewrite each answer into an ASD-STE100 document for the pane
22
23// STE pane values. $.state keeps them across a reload of the module, and a write redraws the pane.
24const view = atom({ plugin: 'asd-ste100', key: 'view' }, null);
25const report = atom({ plugin: 'asd-ste100', key: 'report' }, null);
26const tab = atom({ plugin: 'asd-ste100', key: 'tab' }, 'view');
27const isAsked = atom({ plugin: 'asd-ste100', key: 'isAsked' }, false);
28const isRewriting = atom({ plugin: 'asd-ste100', key: 'isRewriting' }, false);
29const paneOpen = atom({ plugin: 'asd-ste100', key: 'paneOpen' }, false);
30const record = atom({ plugin: 'asd-ste100', key: 'record' }, null);
31const recordReport = atom({ plugin: 'asd-ste100', key: 'recordReport' }, null);
32const isRecording = atom({ plugin: 'asd-ste100', key: 'isRecording' }, false);
33
34// Word limit for one sentence of a report
35const limitOf = (sentence) => (sentence.isProcedural && steMode === 'strict' ? 20 : 25);
36
37const modeLabel = () => (steMode === 'strict' ? 'STRICT 100%' : 'PRAGMATIC 80%');
38const modeName = (mode) => (mode === 'strict' ? 'Strict ASD-STE100' : '80% ASD-STE100 (Karpathy Pragmatic Mode)');
39
40// Ask the model for the rewritten text only, without commentary
41// The structure of an ASD-STE100 document, for the answer itself and for the pane's rewrite
42const documentRules = (mode) => `- Start with a short title line: "# <title>".
43- Give each topic its own "## <heading>". Write a maximum of 6 sentences in each paragraph.
44- Write one idea in each sentence. Do not join two clauses with ", and" or ", so".
45- Write a maximum of ${mode === 'strict' ? 20 : 25} words in each sentence. Use the active voice.
46- Do not use phrasal verbs ("set up" -> "install", "look up" -> "find", "break down" -> "divide").
47- Use simple, literal words. Do not use idioms.
48- Write procedures as numbered steps, with one command in each step.
49- Write risks as "WARNING:" (injury, data loss) or "CAUTION:" (damage) lines.
50- Keep code blocks, code names and technical names unchanged.`;
51
52// The system prompt section while STE mode is on. The structure applies to every explanation.
53const directiveOf = (mode) => `${buildSystemPrompt({ mode })}
54
55ASD-STE100 MODE IS ON. Write every explanation in the chat as an ASD-STE100 document:
56${documentRules(mode)}
57Write in the language of the person's prompt, and apply these rules to that language.
58These rules apply even when the prompt asks for another form, for example "one long paragraph" or "a detailed essay". Give the same detail, but in this structure. The rules apply to your prose only, not to code, commands or file contents that you write with tools.`;
59
60// Ask the model for the answer as an ASD-STE100 document, for the pane
61// The language of a text by its script. The rewrite names it, as "the same language" alone let a model pick a wrong one.
62const langOf = (text) => (/[\uac00-\ud7a3]/.test(text) ? 'Korean'
63  : /[\u3040-\u30ff]/.test(text) ? 'Japanese'
64    : /[\u4e00-\u9fff]/.test(text) ? 'Chinese'
65      : 'English');
66let lastPromptLang = null; // The language of the person's last prompt, for the session record
67
68// Ask for a record of the whole session as an ASD-STE100 document
69const recordPrompt = (mode, lang) => `Write a record of this whole conversation as an ASD-STE100 document in ${modeName(mode)}.
70Write the record in ${lang}. Apply the ASD-STE100 rules to ${lang}: short sentences, one idea in each sentence, the active voice, and simple, literal words.
71Use these sections, and leave out a section that has no content:
72- "## Purpose": what the person wanted.
73- "## Decisions": what the person and the assistant decided, and why.
74- "## Completed work": what the assistant did, as numbered steps in time order.
75- "## Open items": what is not done, as a list.
76Write risks as "WARNING:" or "CAUTION:" lines.
77Follow these rules:
78${documentRules(mode)}
79Write only facts from the conversation. Do not add new facts. Output only the document.`;
80
81const MAX_TRANSCRIPT = 150000;
82
83// The session as plain text, cut from the start when it is too long
84async function transcriptOf($) {
85  const rows = await $.session.messages();
86  const text = rows
87    .filter(m => m.text && m.text.trim())
88    .map(m => `${m.role === 'user' ? 'PERSON' : 'ASSISTANT'}: ${m.text.trim()}`)
89    .join('\n\n');
90  return text.length > MAX_TRANSCRIPT ? `[earlier part of the conversation left out]\n${text.slice(-MAX_TRANSCRIPT)}` : text;
91}
92
93// Make the session record. It runs only when the person asks (/asd session, or g in the pane), as it uses tokens.
94// The full way forks the main model over the cached conversation; lite sends the transcript to a small model.
95async function makeRecord($, lite = false) {
96  if (await read($, isRecording)) return;
97  await update($, isRecording, () => true);
98  await update($, tab, () => 'session');
99  let response = null;
100  let source = lite ? 'lite' : 'fork';
101  const lastView = await read($, view);
102  const recordLang = lastPromptLang || (lastView ? langOf(lastView.original) : 'English');
103  if (!lite) {
104    response = await $.model.fork({ prompt: recordPrompt(steMode, recordLang) });
105    if (!response.isAnswered && response.reason !== 'aborted') source = 'lite';
106  }
107  if (source === 'lite') {
108    const transcript = await transcriptOf($);
109    response = transcript
110      ? await $.model.complete({
111        model: 'haiku',
112        system: buildSystemPrompt({ mode: steMode }),
113        prompt: `${recordPrompt(steMode, recordLang)}\n\nThe conversation:\n\n${transcript}`,
114        maxTokens: 4000,
115        timeoutMs: 120000
116      })
117      : { isAnswered: false, reason: 'nothing-to-fork' };
118  }
119  if (response.isAnswered) {
120    const text = response.text.trim();
121    const date = new Date(await $.clock.now()).toISOString().slice(0, 10);
122    await update($, record, (r) => ({ text, source, date, rev: ((r && r.rev) || 0) + 1 }));
123    await update($, recordReport, () => reportOf(text));
124  } else {
125    $.ui.toast(response.reason === 'nothing-to-fork'
126      ? 'STE session record: the session has no conversation yet.'
127      : `STE session record failed: ${response.reason}`);
128  }
129  await update($, isRecording, () => false);
130}
131
132const documentPrompt = (mode, text) => `Rewrite the text below as an ASD-STE100 document in ${modeName(mode)}.
133Write the document in ${langOf(text)}, the language of the text. Do not translate it into another language. Apply the ASD-STE100 rules to ${langOf(text)}: short sentences, one idea in each sentence, the active voice, and simple, literal words.
134Follow these rules:
135${documentRules(mode)}
136Output only the document.
137
138${text}`;
139
140const rewritePrompt = (mode, text) =>
141  `Rewrite the following text into ${modeName(mode)}. Output only the rewritten text. Do not add headings, explanations, or notes.\n\n${text}`;
142
143// Lint report of an answer, without its code, tables and links
144const reportOf = (text) => {
145  const prose = proseOf(text);
146  const checked = prose ? validateText(prose, { mode: steMode }) : null;
147  return checked && checked.totalSentences > 0 ? checked : null;
148};
149
150async function openPane($) {
151  await update($, paneOpen, () => true);
152  const opened = await $.ui.open({ id: PANE_ID, title: 'STE' });
153  // A pane the mod opens by itself waits below 144 terminal columns. Say so, not an empty screen.
154  if (opened && opened.isPlaced === false) {
155    $.ui.toast('STE pane is ready. Make the terminal 144 columns or wider, or type /asd pane.');
156  }
157  return opened;
158}
159
160// Rewrite the original answer in STE for the pane
161// force: rewrite again when the pane already shows an STE version (the r key)
162async function rewriteView($, force = false) {
163  const current = await read($, view);
164  if (!current || (!force && current.source === 'rewrite')) return;
165  const original = current.original;
166  await update($, isRewriting, () => true);
167  const response = await $.model.complete({
168    model: 'haiku',
169    system: buildSystemPrompt({ mode: steMode }),
170    prompt: documentPrompt(steMode, original),
171    maxTokens: 3000,
172    timeoutMs: 90000
173  });
174  // Keep the result only if the pane still shows the same answer
175  const now = await read($, view);
176  if (now && now.original === original) {
177    if (response.isAnswered) {
178      const text = response.text.trim();
179      await update($, view, (v) => ({ ...v, text, source: 'rewrite', rev: (v.rev || 0) + 1 }));
180      await update($, report, () => reportOf(text));
181    } else {
182      $.ui.toast(`STE rewrite failed: ${response.reason}`);
183    }
184    await update($, isRewriting, () => false);
185  }
186}
187
188// Show the current state in the status line and redraw the spinner suffix
189function refreshUi($) {
190  $.ui.status(steActive ? `STE [${modeLabel()}]: Active` : undefined);
191  $.ui.invalidate('ui.render');
192}
193
194export function register(on, options = {}) {
195  // userConfig values are defaults. A mode or state saved with /asd (machine-wide $.store) overrides them.
196  if (options.defaultMode === 'strict') {
197    steMode = 'strict';
198  }
199  if (options.autoInject) {
200    steActive = true;
201  }
202
203  // Session start: register command, tools, and restore saved state
204  on('session.start', async ($, e, next) => {
205    try {
206      await $.command.register({
207        name: 'asd',
208        description: 'Control ASD-STE100 writing mode, check text, or rewrite content',
209        argumentHint: '[on|off|80|strict|pane [on|off]|auto [on|off]|session [lite]|check <text>|rewrite <text>|status]'
210      });
211
212      await $.tool.register({
213        name: 'validate_ste',
214        description: 'Validate text against ASD-STE100 (Simplified Technical English) rules and return quality metrics and suggestions.',
215        inputSchema: {
216          type: 'object',
217          properties: {
218            text: { type: 'string', description: 'Text to analyze' },
219            mode: { type: 'string', enum: ['pragmatic', 'strict'], description: 'Validation mode (default: pragmatic / 80%)' }
220          },
221          required: ['text']
222        }
223      });
224
225      await $.tool.register({
226        name: 'rewrite_ste',
227        description: 'Rewrite text into ASD-STE100 simplified technical style.',
228        inputSchema: {
229          type: 'object',
230          properties: {
231            text: { type: 'string', description: 'Original text to rewrite' },
232            mode: { type: 'string', enum: ['pragmatic', 'strict'] },
233            format: { type: 'string', enum: ['text', 'diagram', 'html'] }
234          },
235          required: ['text']
236        }
237      });
238
239      // Restore persisted state
240      const savedActive = await $.store.get('ste_active');
241      if (typeof savedActive === 'boolean') steActive = savedActive;
242      const savedMode = await $.store.get('ste_mode');
243      if (savedMode === 'strict' || savedMode === 'pragmatic') steMode = savedMode;
244      const savedPane = await $.store.get('ste_pane');
245      if (typeof savedPane === 'boolean') paneAuto = savedPane;
246      const savedRewrite = await $.store.get('ste_autorewrite');
247      if (typeof savedRewrite === 'boolean') autoRewrite = savedRewrite;
248
249      if (steActive) refreshUi($);
250    } catch (err) {
251      // Avoid failing session start if registration encounters issues
252      $.ui.log(`ASD-STE100 mod initialization notice: ${err.message}`);
253    }
254
255    return next(e);
256  });
257
258  // Handle /asd command
259  on('command.run', { command: 'asd' }, async ($, e) => {
260    const rawArgs = (e.args || '').trim();
261    const first = rawArgs.split(/\s+/)[0] || '';
262    const sub = first.toLowerCase();
263    // Keep the original line breaks of the text after the subcommand
264    const rest = rawArgs.slice(first.length).trim();
265
266    if (sub === 'on') {
267      steActive = true;
268      await $.store.set('ste_active', true);
269      refreshUi($);
270      return { text: `[ASD-STE100] Activated. Mode: ${modeLabel()}. Claude now answers in ${modeName(steMode)}. Use /asd 80 or /asd strict to change the mode.` };
271    }
272
273    if (sub === 'off') {
274      steActive = false;
275      await $.store.set('ste_active', false);
276      refreshUi($);
277      return { text: '[ASD-STE100] Deactivated. Standard generation resumed.' };
278    }
279
280    if (sub === '80' || sub === 'pragmatic') {
281      steMode = 'pragmatic';
282      await $.store.set('ste_mode', 'pragmatic');
283      refreshUi($);
284      return { text: '[ASD-STE100] Mode: 80% Pragmatic (Karpathy style): sentences <= 25 words, active voice, one idea in each sentence. The STE pane uses this mode. Use /asd on to make the answer itself STE.' };
285    }
286
287    if (sub === '100' || sub === 'strict') {
288      steMode = 'strict';
289      await $.store.set('ste_mode', 'strict');
290      refreshUi($);
291      return { text: '[ASD-STE100] Mode: 100% Strict (ASD-STE100 Issue 9): max 20 words for procedures, max 25 for descriptions, approved vocabulary, active voice. The STE pane uses this mode. Use /asd on to make the answer itself STE.' };
292    }
293
294    if (sub === 'session') {
295      const lite = rest.toLowerCase() === 'lite';
296      await openPane($);
297      // Run after the command returns, so the record does not belong to the command's dispatch
298      $.clock.after(0, () => { void makeRecord($, lite); });
299      return { text: `[ASD-STE100] Writing an STE record of this session in the pane${lite ? ' (lite: small model)' : ''}. This uses tokens.` };
300    }
301
302    if (sub === 'auto') {
303      const arg = rest.toLowerCase();
304      autoRewrite = arg === 'on' ? true : arg === 'off' ? false : !autoRewrite;
305      await $.store.set('ste_autorewrite', autoRewrite);
306      return { text: `[ASD-STE100] Auto rewrite ${autoRewrite ? 'ON: the pane shows each answer rewritten as an ASD-STE100 document' : 'OFF: press r in the pane to rewrite an answer'}.` };
307    }
308
309    if (sub === 'pane') {
310      const arg = rest.toLowerCase();
311      if (arg === 'on' || arg === 'off') {
312        paneAuto = arg === 'on';
313        await $.store.set('ste_pane', paneAuto);
314        return { text: `[ASD-STE100] The STE pane ${paneAuto ? 'opens by itself after each answer' : 'opens only with /asd pane'}.` };
315      }
316      await openPane($);
317      return { text: '[ASD-STE100] STE pane opened. v: view · c: check · r: rewrite · x: close' };
318    }
319
320    if (sub === 'status') {
321      return {
322        text: `[ASD-STE100 Status]
323- State: ${steActive ? 'ACTIVE (automatically formatting prompts)' : 'INACTIVE'}
324- Mode: ${modeLabel()}
325- Pane: ${paneAuto ? 'opens by itself after each answer' : 'opens only with /asd pane'}
326- Auto rewrite: ${autoRewrite ? 'ON (one small model call for each answer)' : 'OFF'}
327- Commands: /asd [on|off|80|strict|pane [on|off]|check <text>|rewrite <text>]`
328      };
329    }
330
331    if (sub === 'check') {
332      if (!rest) {
333        return { text: 'Usage: /asd check <text to analyze>' };
334      }
335      const report = validateText(rest, { mode: steMode });
336      let output = `[ASD-STE100 Quality Report - ${modeLabel()}]\nScore: ${report.score}/100 | Sentences: ${report.totalSentences} | Avg Words: ${report.averageWordsPerSentence} | Issues: ${report.totalIssues}\n`;
337      report.sentences.forEach(s => {
338        const icon = s.issues.length === 0 ? '✔' : '⚠';
339        output += `\n${icon} [S${s.index} (${s.wordCount} words)]: "${s.text}"\n`;
340        s.issues.forEach(i => {
341          output += `   • [${i.type}] ${i.message}\n`;
342        });
343      });
344      return { text: output.trim() };
345    }
346
347    if (sub === 'rewrite') {
348      if (!rest) {
349        return { text: 'Usage: /asd rewrite <text to convert to STE>' };
350      }
351      // Call model to rewrite
352      const systemInstruction = buildSystemPrompt({ mode: steMode });
353      const response = await $.model.complete({
354        model: 'haiku',
355        system: systemInstruction,
356        prompt: rewritePrompt(steMode, rest),
357        maxTokens: 1000,
358        timeoutMs: 30000
359      });
360
361      if (response && response.isAnswered) {
362        return { text: `[Rewritten in ASD-STE100 (${modeLabel()})]:\n\n${response.text.trim()}` };
363      }
364      return { text: `Could not rewrite via model. Validation report for original text:\n` + JSON.stringify(validateText(rest, { mode: steMode }), null, 2) };
365    }
366
367    // Default help
368    return {
369      text: `[ASD-STE100 Simplified Technical English Assistant]
370Inspired by Andrej Karpathy's controlled language guidance for LLM understanding.
371
372Commands:
373  /asd on             - Activate automatic STE prompt enhancement
374  /asd off            - Deactivate STE enhancement
375  /asd 80             - Set to 80% Pragmatic Mode (Karpathy style, readable & fast)
376  /asd strict         - Set to 100% Strict ASD-STE100 standard
377  /asd pane           - Show the last answer as an STE document in a side pane
378  /asd pane [on|off]  - Open the pane by itself after each answer, or not
379  /asd auto [on|off]  - Rewrite each answer as an ASD-STE100 document in the pane, or not
380  /asd session [lite] - Write an STE record of the whole session in the pane (on request only; uses tokens)
381  /asd check <text>   - Lint text and inspect compliance score
382  /asd rewrite <text> - Rewrite text into STE format
383  /asd status         - Check current mode and settings`
384    };
385  });
386
387  // Handle validate_ste tool call
388  on('tool.call', { tool: 'mcp__asd-ste100__validate_ste' }, async ($, e) => {
389    const report = validateText(e.text || '', { mode: e.mode || steMode });
390    return { result: JSON.stringify(report, null, 2) };
391  });
392
393  // Handle rewrite_ste tool call
394  on('tool.call', { tool: 'mcp__asd-ste100__rewrite_ste' }, async ($, e) => {
395    const mode = e.mode || steMode;
396    const format = e.format || 'text';
397    const system = buildSystemPrompt({ mode, targetFormat: format });
398    const response = await $.model.complete({
399      model: 'haiku',
400      system,
401      prompt: rewritePrompt(mode, e.text || ''),
402      maxTokens: 1500,
403      timeoutMs: 30000
404    });
405    return { result: response && response.isAnswered ? response.text.trim() : 'Rewriting failed or model timed out.' };
406  });
407
408  // The answer is in STE when the person asks for it or STE mode is on
409  on('prompt.submit', async ($, e, next) => {
410    await update($, isAsked, () => steActive || STE_REQUEST.test(e.text));
411    // A slash command says nothing about the person's language
412    if (!e.text.trim().startsWith('/')) lastPromptLang = langOf(e.text);
413    return next(e);
414  });
415
416  // While STE mode is on, add the STE rules to the system prompt. The person's prompt stays as typed.
417  on('prompt.compose', async ($, e, next) => {
418    const composed = await next(e);
419    if (!steActive) return composed;
420    return {
421      ...composed,
422      sections: [
423        ...composed.sections,
424        { id: 'asd-ste100:directive', text: directiveOf(steMode), scope: 'session' }
425      ]
426    };
427  });
428
429  // Enhance spinner while thinking if STE is active
430  on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
431    if (!steActive) return next(e);
432    const suffix = ` · STE [${modeLabel()}]`;
433    return next({
434      ...e,
435      props: {
436        ...e.props,
437        suffix: (e.props && e.props.suffix ? e.props.suffix : '') + suffix
438      }
439    });
440  });
441
442  // Keep the final answer of each main-loop turn for the STE pane
443  on('turn.complete', async ($, e, next) => {
444    if (!e.agentId && !e.isAborted && e.answer.trim()) {
445      const asked = await read($, isAsked);
446      const previous = await read($, view);
447      const date = new Date(await $.clock.now()).toISOString().slice(0, 10);
448      await update($, view, () => ({
449        text: e.answer, original: e.answer, source: 'answer', isAsked: asked,
450        task: ((previous && previous.task) || 0) + 1, rev: 0, date
451      }));
452      await update($, report, () => reportOf(e.answer));
453      await update($, tab, () => 'view');
454      await update($, isRewriting, () => autoRewrite);
455      // Open it each time: a pane that is already open only keeps its place, and a state left from a resumed session cannot keep it closed
456      if (paneAuto) await openPane($);
457      // Rewrite after the turn ends, so the rewrite does not belong to the turn's dispatch
458      if (autoRewrite) $.clock.after(0, () => { void rewriteView($); });
459    }
460    return next(e);
461  });
462
463  on('ui.close', async ($, e, next) => {
464    if (e.id === PANE_ID) await update($, paneOpen, () => false);
465    return next(e);
466  });
467
468  // Draw the STE pane
469  on('ui.render', { component: 'Pane', requestId: 'ste-sheet' }, async ($, e) => {
470    const els = { h, ...$.ui.resolve(e) };
471    try {
472      return await drawPaneFor($, e, els);
473    } catch (err) {
474      // Show the error in the pane, not an empty pane
475      const { Box, Text } = els;
476      return h(Box, { flexDirection: 'column' },
477        h(Text, { color: 'red' }, 'The STE pane could not draw this answer.'),
478        h(Text, { dimColor: true }, String((err && err.message) || err)));
479    }
480  });
481}
482
483async function drawPaneFor($, e, els) {
484  return drawPane(els, {
485    view: await read($, view),
486    report: await read($, report),
487    tab: await read($, tab),
488    mode: steMode,
489    modeLabel: modeLabel(),
490    limitOf,
491    isRewriting: await read($, isRewriting),
492    record: await read($, record),
493    recordReport: await read($, recordReport),
494    isRecording: await read($, isRecording),
495    onTab: (id) => update($, tab, () => id),
496    onRewrite: () => rewriteView($, true),
497    onRecord: () => makeRecord($),
498    onClose: () => $.ui.close({ id: PANE_ID })
499  }, e.props.bodyColumns);
500}
501
lib/ste-engine.js 627 lines
1/**
2 * ASD-STE100 (Simplified Technical English) Rule Engine & Validator
3 * 
4 * Implements core checking rules based on the ASD-STE100 Standard (Issue 9, January 2025)
5 * and Andrej Karpathy's "80% Pragmatic Mode" for LLM clarity.
6 */
7
8// Common unapproved words in ASD-STE100 and their recommended replacements
9const UNAPPROVED_WORDS = {
10  'utilize': 'use',
11  'utilizes': 'uses',
12  'utilized': 'used',
13  'utilizing': 'using',
14  'utilization': 'use',
15  'terminate': 'stop',
16  'terminates': 'stops',
17  'terminated': 'stopped',
18  'terminating': 'stopping',
19  'commence': 'start',
20  'commences': 'starts',
21  'commenced': 'started',
22  'commencing': 'starting',
23  'initiate': 'start',
24  'initiates': 'starts',
25  'initiated': 'started',
26  'initiating': 'starting',
27  'shut': 'close',
28  'shuts': 'closes',
29  'shutting': 'closing',
30  'modify': 'change',
31  'modifies': 'changes',
32  'modified': 'changed',
33  'modifying': 'changing',
34  'modification': 'change',
35  'prior to': 'before',
36  'subsequent to': 'after',
37  'in order to': 'to',
38  'in the event that': 'if',
39  'in the event of': 'if there is',
40  'as well as': 'and',
41  'adequate': 'sufficient (or give exact value)',
42  'adequately': 'sufficiently',
43  'properly': 'correctly (or give exact instruction)',
44  'etc': 'list all items (do not use etc.)',
45  'and/or': 'and | or (choose one)',
46  'shall': 'must (or use imperative)',
47  'should': 'must (or explain optional choice)',
48  'ought to': 'must',
49  'could': 'can',
50  'might': 'can (or state condition)',
51  'accomplish': 'do / complete',
52  'accomplishes': 'does / completes',
53  'accomplished': 'done / completed',
54  'obtain': 'get',
55  'obtains': 'gets',
56  'obtained': 'got',
57  'obtaining': 'getting',
58  'fabricate': 'make',
59  'fabricates': 'makes',
60  'fabricated': 'made',
61  'approximately': 'about',
62  'subsequently': 'then / after that',
63  'eliminate': 'remove',
64  'eliminates': 'removes',
65  'eliminated': 'removed',
66  'eliminating': 'removing',
67  'execute': 'do / run / start',
68  'executes': 'does / runs / starts',
69  'executed': 'done / ran / started',
70  'transmit': 'send',
71  'transmits': 'sends',
72  'transmitted': 'sent',
73  'transmitting': 'sending',
74  'verify': 'make sure / check',
75  'verifies': 'makes sure / checks',
76  'verified': 'made sure / checked',
77  'inspect': 'examine',
78  'inspects': 'examines',
79  'inspected': 'examined',
80  'ascertain': 'make sure / find',
81  'furthermore': 'also',
82  'moreover': 'also',
83  'nevertheless': 'but / however',
84  'nonetheless': 'but / however',
85  'in view of the fact that': 'because',
86  'for the purpose of': 'to',
87  'with the exception of': 'except',
88  'at the present time': 'now',
89  'has the capability to': 'can',
90  'is able to': 'can',
91  'it is recommended that': 'we recommend that (or use imperative)',
92  'it is necessary to': 'you must',
93  'facilitate': 'help / make easy',
94  'facilitates': 'helps',
95  'facilitated': 'helped',
96  'hazardous': 'dangerous',
97  'magnitude': 'size / value',
98  'necessitate': 'need / require',
99  'numerous': 'many',
100  'optimum': 'best',
101  'portion': 'part',
102  'portions': 'parts',
103  'precaution': 'safety rule',
104  'preclude': 'prevent',
105  'precludes': 'prevents',
106  'precluded': 'prevented',
107  'prescribed': 'specified',
108  'proximity': 'near',
109  'rectify': 'correct / repair',
110  'rectifies': 'corrects / repairs',
111  'rectified': 'corrected / repaired',
112  'remainder': 'rest',
113  'retain': 'keep',
114  'retains': 'keeps',
115  'retained': 'kept',
116  'retaining': 'keeping',
117  'supersede': 'replace',
118  'supersedes': 'replaces',
119  'superseded': 'replaced',
120  'vicinity': 'near / area',
121  'vital': 'important / necessary'
122};
123
124// Common past participles used in passive voice detection
125const PASSIVE_PARTICIPLES = new Set([
126  'given', 'taken', 'done', 'seen', 'made', 'written', 'executed', 'performed',
127  'sent', 'received', 'found', 'installed', 'removed', 'replaced', 'adjusted',
128  'checked', 'tested', 'connected', 'disconnected', 'set', 'opened', 'closed',
129  'transmitted', 'processed', 'calculated', 'generated', 'modified', 'changed',
130  'verified', 'examined', 'configured', 'initialized', 'stopped', 'started',
131  'loaded', 'saved', 'deleted', 'created', 'updated', 'selected', 'cleaned'
132]);
133
134const BE_VERBS = new Set([
135  'is', 'are', 'was', 'were', 'be', 'been', 'being', 'become', 'becomes', 'became'
136]);
137
138// Participles that usually act as adjectives after "be" ("The users are tired.")
139const ADJECTIVAL_PARTICIPLES = new Set([
140  'tired', 'interested', 'excited', 'pleased', 'bored', 'worried', 'surprised',
141  'satisfied', 'concerned', 'involved', 'supposed', 'scared', 'confused', 'married'
142]);
143
144// Words ending in -ing that are not progressive verb forms ("The type is string.")
145const NON_PROGRESSIVE_ING = new Set([
146  'nothing', 'something', 'anything', 'everything', 'string', 'thing', 'ring',
147  'king', 'during', 'spring', 'morning', 'evening', 'ceiling', 'building', 'missing', 'interesting'
148]);
149
150// Nouns that end in -ed ("RSS feed", "network speed")
151const EED_NOUNS = new Set(['speed', 'feed', 'seed', 'need', 'breed', 'greed']);
152
153const HAVE_VERBS =new Set(['has', 'have', 'had']);
154
155// Abbreviations that end with a period but do not end a sentence
156const ABBREVIATIONS = new Set([
157  'mr', 'mrs', 'ms', 'dr', 'prof', 'sr', 'jr', 'vs', 'e.g', 'i.e', 'approx', 'fig'
158]);
159
160/**
161 * Split text into individual sentences.
162 * Decimals ("2.5") and common abbreviations ("Mr.", "e.g.") do not end a sentence.
163 */
164function splitSentences(text) {
165  if (!text) return [];
166  const sentences = [];
167  for (const line of text.split(/\n+/)) {
168    let current = '';
169    for (const part of line.split(/(?<=[.!?])\s+/)) {
170      current = current ? `${current} ${part}` : part;
171      const lastWord = (current.match(/(\S+)\.$/) || [])[1];
172      if (lastWord && ABBREVIATIONS.has(lastWord.toLowerCase())) continue;
173      sentences.push(current);
174      current = '';
175    }
176    if (current) sentences.push(current);
177  }
178  return sentences.map(s => s.trim()).filter(Boolean);
179}
180
181/**
182 * Split sentence into clean tokens (words).
183 */
184function tokenizeWords(sentence) {
185  return sentence
186    .replace(/[^\w\s-]/g, ' ')
187    .split(/\s+/)
188    .filter(w => w.length > 0);
189}
190
191/**
192 * Detect passive voice in a sentence.
193 */
194function detectPassiveVoice(words) {
195  const issues = [];
196  const cleanWords = words.map(w => w.replace(/[^\w-]/g, ''));
197  const lower = cleanWords.map(w => w.toLowerCase());
198  for (let i = 0; i < lower.length - 1; i++) {
199    if (BE_VERBS.has(lower[i])) {
200      // Check next word or next-next word (in case of adverb e.g. "is carefully tested")
201      const nextWord = lower[i + 1];
202      const nextNextWord = lower[i + 2];
203      
204      const isPastForm = (w) => Boolean(w && !ADJECTIVAL_PARTICIPLES.has(w) &&
205        (PASSIVE_PARTICIPLES.has(w) || (w.endsWith('ed') && w.length > 3)));
206      
207      if (isPastForm(nextWord)) {
208        issues.push({
209          phrase: `${cleanWords[i]} ${cleanWords[i + 1]}`,
210          index: i,
211          suggestion: 'Convert to active voice (state who or what performs the action).'
212        });
213      } else if (nextNextWord && isPastForm(nextNextWord)) {
214        issues.push({
215          phrase: `${cleanWords[i]} ${cleanWords[i + 1]} ${cleanWords[i + 2]}`,
216          index: i,
217          suggestion: 'Convert to active voice (state who or what performs the action).'
218        });
219      }
220    }
221  }
222  return issues;
223}
224
225/**
226 * Detect verb tenses that ASD-STE100 does not permit:
227 * progressive ("is running") and perfect ("has processed").
228 */
229function detectUnapprovedTenses(words) {
230  const issues = [];
231  const lower = words.map(w => w.toLowerCase());
232  for (let i = 0; i < lower.length - 1; i++) {
233    const next = lower[i + 1];
234    if (BE_VERBS.has(lower[i]) && lower[i] !== 'being' && next.endsWith('ing') && next.length > 4 && !NON_PROGRESSIVE_ING.has(next)) {
235      issues.push({ phrase: `${words[i]} ${words[i + 1]}`, tense: 'progressive', suggestion: 'Use the present simple (e.g. "is running" -> "runs").' });
236    }
237    if (HAVE_VERBS.has(lower[i]) && (next.endsWith('ed') || PASSIVE_PARTICIPLES.has(next) || next === 'been')) {
238      issues.push({ phrase: `${words[i]} ${words[i + 1]}`, tense: 'perfect', suggestion: 'Use the past simple (e.g. "has processed" -> "processed").' });
239    }
240  }
241  return issues;
242}
243
244// Phrasal verbs (STE 9.3) and their single-verb replacements
245const PHRASAL_VERBS = [
246  [/\b(set|sets|setting) up\b/gi, 'install / prepare / configure'],
247  [/\b(carry|carries|carried|carrying) out\b/gi, 'do'],
248  [/\b(find|finds|found|finding) out\b/gi, 'find / learn'],
249  [/\b(figure|figures|figured|figuring) out\b/gi, 'find / calculate'],
250  [/\b(look|looks|looked|looking) into\b/gi, 'examine'],
251  [/\b(come|comes|came|coming) up with\b/gi, 'make / find'],
252  [/\b(get|gets|got|getting) rid of\b/gi, 'remove'],
253  [/\b(fill|fills|filled|filling) out\b/gi, 'complete'],
254  [/\b(give|gives|gave|giving) up\b/gi, 'stop']
255];
256
257/**
258 * Detect punctuation and style issues: semicolons (STE 8.1), contractions (STE 4.2),
259 * Latin abbreviations (grammar rules) and phrasal verbs (STE 9.3).
260 * Contractions and Latin abbreviations are reported in strict mode only.
261 */
262function detectStyleIssues(sentence, isStrict) {
263  const issues = [];
264  if (sentence.includes(';')) {
265    issues.push({
266      type: 'SEMICOLON',
267      severity: isStrict ? 'error' : 'info',
268      rule: 'STE 8.1: No Semicolons',
269      message: 'Semicolon detected. ASD-STE100 does not permit semicolons.',
270      suggestion: 'Write two sentences.'
271    });
272  }
273  if (isStrict) {
274    const contractions = sentence.match(/\b\w+(n't|'re|'ll|'ve|'d|'m)\b|\b(it|that|there|what|here)'s\b/gi) || [];
275    for (const c of contractions) {
276      issues.push({
277        type: 'CONTRACTION',
278        severity: 'warning',
279        rule: 'STE 4.2: No Contractions',
280        message: `Contraction detected: "${c}".`,
281        suggestion: 'Write the full form (e.g. "don\'t" -> "do not").'
282      });
283    }
284    const latin = sentence.match(/\b(e\.g|i\.e|viz|cf)\.?(?=\s|,|$)/gi) || [];
285    for (const l of latin) {
286      issues.push({
287        type: 'LATIN_ABBREVIATION',
288        severity: 'warning',
289        rule: 'STE Grammar: Latin Abbreviations',
290        message: `Latin abbreviation detected: "${l}".`,
291        suggestion: 'Use "for example" or "that is".'
292      });
293    }
294  }
295  for (const [regex, replacement] of PHRASAL_VERBS) {
296    for (const match of sentence.match(regex) || []) {
297      issues.push({
298        type: 'PHRASAL_VERB',
299        severity: isStrict ? 'warning' : 'info',
300        rule: 'STE 9.3: No Phrasal Verbs',
301        message: `Phrasal verb detected: "${match}". Use "${replacement}" instead.`,
302        word: match,
303        replacement
304      });
305    }
306  }
307  return issues;
308}
309
310/**
311 * Check unapproved words and multi-word phrases.
312 */
313function detectUnapprovedWords(sentence) {
314  const issues = [];
315  const lowerSentence = ` ${sentence.toLowerCase()} `;
316  
317  // Check multi-word phrases and entries with punctuation ("and/or") first
318  for (const [phrase, replacement] of Object.entries(UNAPPROVED_WORDS)) {
319    if (/[^a-z]/.test(phrase)) {
320      const regex = new RegExp(`\\b${phrase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'gi');
321      let match;
322      while ((match = regex.exec(sentence)) !== null) {
323        issues.push({
324          word: match[0],
325          replacement,
326          rule: 'STE 1.1: Approved Words',
327          explanation: `"${match[0]}" is unapproved in ASD-STE100. Use "${replacement}" instead.`
328        });
329      }
330    }
331  }
332  
333  // Check single words
334  const words = tokenizeWords(sentence);
335  for (const word of words) {
336    const wLower = word.toLowerCase();
337    // Object.hasOwn: a word such as "constructor" must not match the built-in properties of an object
338    if (Object.hasOwn(UNAPPROVED_WORDS, wLower) && !/[^a-z]/.test(wLower)) {
339      issues.push({
340        word,
341        replacement: UNAPPROVED_WORDS[wLower],
342        rule: 'STE 1.1: Approved Words',
343        explanation: `"${word}" is unapproved in ASD-STE100. Use "${UNAPPROVED_WORDS[wLower]}" instead.`
344      });
345    }
346  }
347  
348  return issues;
349}
350
351/**
352 * Detect excessive noun clusters (4 or more nouns without prepositions).
353 * Simplified heuristic checking consecutive capitalized or noun-like words.
354 */
355function detectNounClusters(words) {
356  const issues = [];
357  // Exclude common grammatical function words
358  const nonNouns = new Set([
359    'the', 'a', 'an', 'in', 'on', 'at', 'to', 'for', 'with', 'by', 'of', 'from',
360    'into', 'onto', 'over', 'under', 'up', 'down', 'out', 'off', 'about', 'through',
361    'between', 'before', 'after', 'during', 'until', 'while', 'because', 'than', 'as',
362    'and', 'or', 'but', 'so', 'not', 'no', 'all', 'each', 'every', 'some', 'any',
363    'is', 'are', 'was', 'were', 'be', 'been', 'being', 'has', 'have', 'had',
364    'do', 'does', 'did', 'will', 'must', 'can', 'may',
365    'if', 'when', 'then', 'now', 'there', 'here', 'also', 'only', 'very',
366    'that', 'which', 'who', 'this', 'these', 'those',
367    'it', 'its', 'they', 'them', 'their', 'you', 'your', 'we', 'our', 'he', 'she', 'his', 'her'
368  ]);
369
370  let currentCluster = [];
371  for (const word of words) {
372    const clean = word.toLowerCase();
373    // Adverbs ("-ly"), past-tense verbs ("-ed") and numbers also break a noun cluster
374    const isPastVerb = clean.endsWith('ed') && clean.length > 4 && !EED_NOUNS.has(clean);
375    if (!nonNouns.has(clean) && clean.length > 1 && !/^\d+$/.test(clean) && !clean.endsWith('ly') && !isPastVerb) {
376      currentCluster.push(word);
377    } else {
378      if (currentCluster.length >= 4) {
379        issues.push({
380          cluster: currentCluster.join(' '),
381          length: currentCluster.length,
382          rule: 'STE 2.1: Multi-Word Nouns',
383          suggestion: `Noun cluster has ${currentCluster.length} words (max allowed is 3). Insert prepositions (e.g. "of", "for") to separate.`
384        });
385      }
386      currentCluster = [];
387    }
388  }
389  if (currentCluster.length >= 4) {
390    issues.push({
391      cluster: currentCluster.join(' '),
392      length: currentCluster.length,
393      rule: 'STE 2.1: Multi-Word Nouns',
394      suggestion: `Noun cluster has ${currentCluster.length} words (max allowed is 3). Insert prepositions to separate.`
395    });
396  }
397  return issues;
398}
399
400/**
401 * Validate a text against ASD-STE100 guidelines.
402 * 
403 * @param {string} text - Input text
404 * @param {object} options - { mode: 'strict' | 'pragmatic', sentenceType: 'procedural' | 'descriptive' }
405 */
406function validateText(text, options = {}) {
407  const mode = options.mode || 'pragmatic'; // default to Karpathy's 80% pragmatic mode
408  const isStrict = mode === 'strict';
409  // Strict: 20 procedural / 25 descriptive. Pragmatic (80%): hard cap of 25 for all sentences.
410  const maxWordsProcedural = isStrict ? 20 : 25;
411  const maxWordsDescriptive = 25;
412
413  const sentences = splitSentences(text);
414  const totalSentences = sentences.length;
415  let totalWords = 0;
416  const sentenceReports = [];
417  let totalIssues = 0;
418
419  sentences.forEach((sentence, index) => {
420    const words = tokenizeWords(sentence);
421    const wordCount = words.length;
422    totalWords += wordCount;
423    
424    // Heuristic: imperative starts often indicate procedural
425    const isProcedural = /^(click|press|turn|open|close|remove|install|check|start|stop|connect|disconnect|run|set|make|do)\b/i.test(words[0] || '');
426    const limit = isProcedural ? maxWordsProcedural : maxWordsDescriptive;
427    
428    const issues = [];
429    
430    // 1. Sentence length check
431    if (wordCount > limit) {
432      issues.push({
433        type: 'SENTENCE_LENGTH',
434        severity: 'warning',
435        rule: isProcedural ? 'STE 5.1: Sentence Length (Procedures)' : 'STE 6: Sentence Length (Descriptions)',
436        message: `Sentence has ${wordCount} words. Maximum allowed for ${isProcedural ? 'procedural' : 'descriptive'} text is ${limit} words.`,
437        suggestion: 'Split into two or more short sentences.'
438      });
439    }
440    
441    // 2. Passive voice check. STE 3.6 permits the passive in descriptive text only when the agent is unknown.
442    const passiveIssues = detectPassiveVoice(words);
443    const namesAgent = /\bby\b/i.test(sentence);
444    for (const p of passiveIssues) {
445      const permitted = !isProcedural && !namesAgent;
446      issues.push({
447        type: 'PASSIVE_VOICE',
448        severity: permitted ? 'info' : (isStrict ? 'error' : 'warning'),
449        permitted,
450        rule: 'STE 3.6: Active Voice',
451        message: permitted
452          ? `Passive construction: "${p.phrase}". Permitted in descriptive text only if the agent is unknown.`
453          : `Passive construction detected: "${p.phrase}".`,
454        suggestion: p.suggestion
455      });
456    }
457
458    // 2b. Punctuation, contractions, Latin abbreviations and phrasal verbs
459    for (const s of detectStyleIssues(sentence, isStrict)) {
460      issues.push(s);
461    }
462    
463    // 3. Unapproved words
464    const unapproved = detectUnapprovedWords(sentence);
465    for (const u of unapproved) {
466      // In pragmatic mode, some softer words like 'should' or 'etc' might be tolerated if user asks, but standard replacements are always helpful
467      issues.push({
468        type: 'UNAPPROVED_WORD',
469        severity: isStrict ? 'error' : 'info',
470        rule: u.rule,
471        message: u.explanation,
472        word: u.word,
473        replacement: u.replacement
474      });
475    }
476    
477    // 4. Verb tenses (strict mode only: pragmatic mode tolerates progressive and perfect forms)
478    if (isStrict) {
479      for (const t of detectUnapprovedTenses(words)) {
480        issues.push({
481          type: 'VERB_TENSE',
482          severity: 'warning',
483          rule: 'STE 3.1: Verb Forms',
484          message: `Unapproved ${t.tense} tense: "${t.phrase}".`,
485          suggestion: t.suggestion
486        });
487      }
488    }
489
490    // 5. Noun clusters
491    const nounClusters = detectNounClusters(words);
492    for (const nc of nounClusters) {
493      issues.push({
494        type: 'NOUN_CLUSTER',
495        severity: 'warning',
496        rule: nc.rule,
497        message: nc.suggestion,
498        cluster: nc.cluster
499      });
500    }
501    
502    // Permitted constructions are reported for review but do not lower the score
503    totalIssues += issues.filter(i => !i.permitted).length;
504    
505    sentenceReports.push({
506      index: index + 1,
507      text: sentence,
508      wordCount,
509      isProcedural,
510      issues
511    });
512  });
513
514  // Paragraph length (Section 6): one topic and maximum 6 sentences per paragraph
515  let offset = 0;
516  for (const paragraph of text.split(/\n\s*\n/)) {
517    const count = splitSentences(paragraph).length;
518    if (count > 6 && sentenceReports[offset + count - 1]) {
519      sentenceReports[offset + count - 1].issues.push({
520        type: 'PARAGRAPH_LENGTH',
521        severity: isStrict ? 'warning' : 'info',
522        rule: 'STE 6: Paragraph Length',
523        message: `Paragraph has ${count} sentences. Maximum is 6 sentences per paragraph.`,
524        suggestion: 'Split the paragraph. Keep one topic in each paragraph.'
525      });
526      totalIssues += 1;
527    }
528    offset += count;
529  }
530
531  // Calculate compliance score (0 - 100)
532  const penalty = totalIssues * 8;
533  const score = Math.max(0, Math.min(100, Math.round(100 - (totalSentences > 0 ? (penalty / totalSentences) : 0))));
534
535  return {
536    mode,
537    score,
538    totalSentences,
539    totalWords,
540    averageWordsPerSentence: totalSentences > 0 ? +(totalWords / totalSentences).toFixed(1) : 0,
541    totalIssues,
542    sentences: sentenceReports
543  };
544}
545
546/**
547 * Generate a comprehensive prompt instruction for LLMs to generate ASD-STE100 output.
548 * 
549 * @param {object} config - { mode: 'strict' | 'pragmatic', targetFormat: 'text' | 'diagram' | 'html' }
550 */
551function buildSystemPrompt(config = {}) {
552  const mode = config.mode || 'pragmatic';
553  const targetFormat = config.targetFormat || 'text';
554
555  if (mode === 'strict') {
556    return `You must write all explanations in strict ASD-STE100 (Simplified Technical English, Issue 9).
557Follow these exact constraints:
5581. WORD CHOICE:
559   - Use only approved words with their approved meanings.
560   - Do NOT use: "utilize" (use "use"), "terminate" (use "stop"), "initiate" (use "start"), "modify" (use "change"), "prior to" (use "before"), "as well as" (use "and").
561   - Eliminate vague words: "adequate", "properly", "etc.", "and/or".
5622. SENTENCE LENGTH:
563   - Maximum 20 words for procedural instructions.
564   - Maximum 25 words for descriptive/explanatory sentences.
5653. SENTENCE STRUCTURE & VOICE:
566   - Write ONE thought per sentence.
567   - Use the ACTIVE VOICE (for example, write "The server sends the request" NOT "The request is sent by the server"). In descriptive text, use the passive voice only when the agent is unknown. In procedures, always use the active voice.
568   - For instructions, use the imperative mood ("Push the button", "Open the file").
5694. NOUN CLUSTERS:
570   - Do NOT use more than 3 consecutive nouns. Separate long noun chains using prepositions.
5715. VERBS:
572   - Use only: infinitive, imperative, simple present, simple past, simple future ("will"), and the past participle as an adjective.
573   - Do NOT use "-ing" forms (except in technical nouns), perfect tenses, or phrasal verbs ("set up", "carry out").
5746. CLARITY AND PUNCTUATION:
575   - Avoid complex clauses. Use bullet lists or tables for steps or multi-item data.
576   - Do not omit articles or "that". Do not use contractions, semicolons, or Latin abbreviations (e.g., i.e., etc.).
577   - Write a maximum of 6 sentences per paragraph, with one topic in each paragraph.
5787. SAFETY:
579   - For risks, write "WARNING:" or "CAUTION:", then a clear command, then a short explanation of the risk.`;
580  }
581
582  // Karpathy 80% Pragmatic Mode
583  return `You must explain this using the "80% ASD-STE100" writing style (Simplified Technical English relaxed for high readability and technical concepts, as recommended by Andrej Karpathy).
584
585Key constraints:
5861. CLEAN, DIRECT SENTENCES:
587   - Keep sentences short and punchy (target 15-22 words, absolute maximum 25 words).
588   - One clear idea per sentence.
5892. ACTIVE VOICE:
590   - Always state who or what performs the action. Avoid passive constructions.
5913. CLEAR VOCABULARY:
592   - Prefer simple, unambiguous verbs ("use" instead of "utilize", "stop" instead of "terminate", "start" instead of "commence/initiate", "before" instead of "prior to").
593   - Strip out fluff, filler, and bureaucratic hedges ("it should be noted that", "in order to", "etc.").
5944. COGNITIVE SPEED:
595   - Structure explanations so readers can scan and parse them instantly.
596   - Limit noun chains to 3 words max. Break them up with prepositions.
597   - Use bold lead-ins and structured bullet points for multi-step logic.
598${targetFormat === 'diagram' ? '5. DIAGRAMS: Complement the text with a clear, concise Mermaid diagram.' : ''}
599${targetFormat === 'html' ? '5. WEB PAGE: Format the output as a clean, responsive, interactive single-file HTML component.' : ''}`;
600}
601
602export {
603  UNAPPROVED_WORDS,
604  splitSentences,
605  tokenizeWords,
606  detectPassiveVoice,
607  detectUnapprovedWords,
608  detectNounClusters,
609  detectUnapprovedTenses,
610  detectStyleIssues,
611  validateText,
612  buildSystemPrompt
613};
614
615export default {
616  UNAPPROVED_WORDS,
617  splitSentences,
618  tokenizeWords,
619  detectPassiveVoice,
620  detectUnapprovedWords,
621  detectNounClusters,
622  detectUnapprovedTenses,
623  detectStyleIssues,
624  validateText,
625  buildSystemPrompt
626};
627
hooks/pane.jsx 236 lines
1/**
2 * STE pane: the last answer as an ASD-STE100 document, docked beside the transcript.
3 *
4 *   v  view      the answer rewritten as an ASD-STE100 document, laid out as a manual page
5 *   o  original  the answer as Claude wrote it
6 *   c  check     the score and the findings of the text in view
7 *   r  rewrite   rewrite the answer again
8 *   s  session   the STE record of the whole session; g makes it (on request only, as it uses tokens)
9 *   x  close
10 */
11
12import { validateText } from '../lib/ste-engine.js';
13import { clip, findingRows, findingsOf, lengthRows, proseOf, widthOf } from './sheet.jsx';
14import { blocksOf, manualRows, titleOf, viewRows } from './view.jsx';
15
16export const PANE_ID = 'ste-sheet';
17
18const scoreColor = (score) => (score >= 90 ? 'green' : score >= 70 ? 'yellow' : 'red');
19
20function checkRows(els, report, limitOf, columns) {
21  const { h, Text } = els;
22  const filled = Math.round((report.score / 100) * columns);
23  const over = report.sentences.filter(s => s.wordCount > limitOf(s)).length;
24  return [
25    <Text key="score">
26      <Text dimColor>score </Text>
27      <Text bold color={scoreColor(report.score)}>{`${report.score}/100`}</Text>
28    </Text>,
29    <Text key="gauge">
30      <Text color={scoreColor(report.score)}>{'█'.repeat(filled)}</Text>
31      <Text dimColor>{'░'.repeat(columns - filled)}</Text>
32    </Text>,
33    <Text key="stats" dimColor>{clip(`${report.totalSentences} sentences · avg ${report.averageWordsPerSentence} words · ${report.totalIssues} issues`, columns)}</Text>,
34    <Text key="gap1"> </Text>,
35    <Text key="len" bold>{'Longest sentences '}<Text dimColor>{over ? `${over} over limit` : 'all within limit'}</Text></Text>,
36    ...lengthRows(els, report, limitOf, Math.max(8, columns - 14)),
37    <Text key="gap2"> </Text>,
38    <Text key="find" bold>Findings</Text>,
39    ...findingRows(els, findingsOf(report), columns, 30),
40  ];
41}
42
43// What one text measures for the change summary
44export function statsOf(text, mode, limitOf) {
45  const prose = proseOf(text);
46  const report = prose ? validateText(prose, { mode }) : { sentences: [], averageWordsPerSentence: 0 };
47  const count = (type) => report.sentences.reduce((n, s) => n + s.issues.filter(i => i.type === type && !i.permitted).length, 0);
48  return {
49    sentences: report.sentences.length,
50    avgWords: Math.round(report.averageWordsPerSentence),
51    tooLong: report.sentences.filter(s => s.wordCount > limitOf(s)).length,
52    tables: blocksOf(text).filter(b => b.kind === 'table').length,
53    phrasal: count('PHRASAL_VERB'),
54    passive: count('PASSIVE_VOICE'),
55    unapproved: count('UNAPPROVED_WORD'),
56  };
57}
58
59// One line that says what the STE rewrite changed, for example "avg words 19 → 7"
60function changeRows(els, before, after, columns) {
61  const { h, Text } = els;
62  const items = [
63    ['sentences', before.sentences, after.sentences],
64    ['avg words', before.avgWords, after.avgWords],
65    ['too long', before.tooLong, after.tooLong],
66    ['tables', before.tables, after.tables],
67    ['phrasal verbs', before.phrasal, after.phrasal],
68    ['passive', before.passive, after.passive],
69    ['unapproved words', before.unapproved, after.unapproved],
70  ].filter(([, a, b]) => a !== b);
71  if (items.length === 0) {
72    return [<Text key="chg-none" dimColor>Original → STE: no measured change</Text>];
73  }
74  const parts = items.map(([label, a, b], n) => (
75    <Text key={`chg-${n}`}>
76      {n > 0 ? <Text dimColor> · </Text> : null}
77      <Text dimColor>{`${label} `}</Text>
78      <Text>{`${a} → `}</Text>
79      <Text color={label === 'sentences' ? undefined : b < a ? 'green' : 'yellow'}>{String(b)}</Text>
80    </Text>
81  ));
82  return [<Text key="chg"><Text dimColor>Original → STE: </Text>{parts}</Text>];
83}
84
85// A row of a text grid: each cell is [label, value], drawn between │ marks to the given widths
86function gridRow({ h, Text }, key, cells, widths) {
87  const parts = [<Text key="l" dimColor>│</Text>];
88  cells.forEach(([label, value, style = {}], i) => {
89    const room = widths[i] - 2;
90    const lab = label ? `${label} ` : '';
91    const val = clip(String(value), Math.max(1, room - lab.length));
92    parts.push(<Text key={`c${i}`}>{' '}<Text dimColor>{lab}</Text><Text {...style}>{val}</Text>{' '.repeat(Math.max(0, room - lab.length - widthOf(val)))}{' '}</Text>);
93    parts.push(<Text key={`s${i}`} dimColor>│</Text>);
94  });
95  return <Text key={key} wrap="truncate-end">{parts}</Text>;
96}
97
98// A border line of a text grid: left, cross and right marks, with ─ to the given widths
99const gridLine = ({ h, Text }, key, widths, [left, cross, right]) => (
100  <Text key={key} dimColor wrap="truncate-end">{left + widths.map(w => '─'.repeat(w)).join(cross) + right}</Text>
101);
102
103// Split `total` cells into widths for n columns, with the n + 1 border marks
104function splitWidths(total, shares) {
105  const room = total - shares.length - 1;
106  const sum = shares.reduce((a, b) => a + b, 0);
107  const widths = shares.map(x => Math.max(4, Math.floor((room * x) / sum)));
108  widths[widths.length - 1] += room - widths.reduce((a, b) => a + b, 0);
109  return widths;
110}
111
112/**
113 * Draw the pane as a page of a maintenance manual:
114 *   title block   ASD-STE100, task number, mode, document title, status
115 *   body          the STE document, numbered 1. / A. / (1) / (a)
116 *   info block    change summary, score, revision, date, page
117 * Below 50 cells the page has no outer frame, so the text keeps its width.
118 * @param {object} els  - the surface's element table ($.ui.resolve(e)) plus `h`
119 * @param {object} pane - { view, report, tab, mode, modeLabel, limitOf, isRewriting, onTab, onRewrite, onClose }
120 *   view: { text, original, source: 'answer' | 'rewrite', isAsked, task, rev, date } | null
121 * @param {number} columns - cells across the pane body
122 */
123export function drawPane(els, pane, columns) {
124  const { h, Box, Text, Button } = els;
125  const { view, report, tab, mode, modeLabel, limitOf, isRewriting, record, recordReport, isRecording, onTab, onRewrite, onRecord, onClose } = pane;
126  const width = Math.max(20, columns);
127  const isFramed = width >= 50;
128  const inner = width;
129  // The body sits inside one cell of padding on each side of a framed page
130  const bodyWidth = isFramed ? width - 2 : width;
131
132  let status;
133  let body = [];
134  let info = [];
135  const isSession = tab === 'session';
136  if (isSession) {
137    if (isRecording) {
138      status = <Text color="yellow">Writing the STE record of this session…</Text>;
139    } else if (record) {
140      status = <Text><Text color="green">Session record</Text><Text dimColor>{record.source === 'lite' ? ' · lite (small model)' : ' · full context'} · g: write again</Text></Text>;
141    } else {
142      status = <Text dimColor>No session record yet. Press g or type /asd session to write one. This uses tokens.</Text>;
143    }
144    if (record) body = manualRows(els, record.text, { mode, limitOf, columns: bodyWidth });
145  } else if (!view) {
146    status = <Text dimColor>The pane shows the next answer as an ASD-STE100 document.</Text>;
147  } else if (tab === 'original') {
148    status = <Text dimColor>Original answer, as Claude wrote it</Text>;
149    body = viewRows(els, view.original, { mode, limitOf, columns: bodyWidth });
150  } else if (isRewriting && view.source !== 'rewrite') {
151    status = <Text color="yellow">Rewriting the answer as an ASD-STE100 document…</Text>;
152    body = [<Text key="wait" dimColor>Press o to read the original answer now.</Text>];
153  } else {
154    status = view.source === 'rewrite'
155      ? <Text color="green">STE version</Text>
156      : <Text><Text color="yellow">Original answer, not rewritten</Text><Text dimColor> · r: rewrite</Text></Text>;
157    body = tab === 'check' && report
158      ? checkRows(els, report, limitOf, bodyWidth)
159      : manualRows(els, view.text, { mode, limitOf, columns: bodyWidth });
160    if (view.source === 'rewrite') {
161      info = changeRows(els, statsOf(view.original, mode, limitOf), statsOf(view.text, mode, limitOf), inner);
162    }
163  }
164
165  // The page shows the session record on the session tab, else the last answer
166  const doc = isSession
167    ? { text: record ? record.text : '', task: `00-02-${String((record && record.rev) || 0).padStart(2, '0')}`, rev: record ? record.rev : 0, date: record && record.date, report: recordReport, fallback: 'Session record' }
168    : { text: view ? view.text : '', task: `00-01-${String((view && view.task) || 0).padStart(2, '0')}`, rev: view ? view.rev || 0 : 0, date: view && view.date, report, fallback: 'Last answer' };
169  const taskNo = doc.task;
170  const title = (titleOf(doc.text) || doc.fallback).toUpperCase();
171  // AMM page blocks: 001 description and operation, 201 maintenance practices (a text with steps)
172  const hasSteps = blocksOf(doc.text).some(b => b.kind === 'step');
173  const block = hasSteps ? ['201', 'MAINTENANCE PRACTICES'] : ['001', 'DESCRIPTION & OPERATION'];
174  const rev = String(doc.rev);
175  const pageReport = doc.report;
176  const score = pageReport ? `${pageReport.score}/100` : '—';
177
178  let header;
179  let footer;
180  if (isFramed) {
181    const top = splitWidths(inner, [3, 1]);
182    const cells = splitWidths(inner, [3, 2, 1, 2]);
183    const foot = splitWidths(inner, [3, 2, 3]);
184    header = [
185      gridLine(els, 'h0', top, ['┌', '┬', '┐']),
186      gridRow(els, 'h1', [['', 'ASD-STE100  STE MAINTENANCE MANUAL', { bold: true }], ['TASK', taskNo, { bold: true }]], top),
187      gridRow(els, 'h2', [['', title, { bold: true, color: 'cyan' }], ['PAGE BLOCK', block[0]]], top),
188      gridLine(els, 'h3', top, ['├', '┴', '┤']),
189      gridRow(els, 'h4', [['', block[1]]], [inner - 2]),
190      gridLine(els, 'h5', cells, ['├', '┬', '┤']),
191      gridRow(els, 'h6', [['EFFECTIVITY', 'ALL'], ['MODE', /STRICT/.test(modeLabel) ? 'STRICT 100%' : '80% STE'], ['REV', rev, { bold: true }], ['DATE', doc.date || '—']], cells),
192      gridLine(els, 'h7', cells, ['└', '┴', '┘']),
193    ];
194    footer = [
195      gridLine(els, 'f0', foot, ['┌', '┬', '┐']),
196      gridRow(els, 'f1', [['', 'STE100 ISSUE 9'], ['SCORE', score, pageReport ? { color: scoreColor(pageReport.score), bold: true } : {}], ['', `${taskNo}  PAGE ${block[0]}`]], foot),
197      gridLine(els, 'f2', foot, ['└', '┴', '┘']),
198    ];
199  } else {
200    header = [
201      <Text key="h1"><Text inverse bold> ASD-STE100 </Text><Text bold>{`  TASK ${taskNo}`}</Text></Text>,
202      <Text key="h2" bold color="cyan">{title}</Text>,
203      <Text key="h3" dimColor>{`${block[1]} · ${modeLabel} · REV ${rev}`}</Text>,
204    ];
205    footer = [<Text key="f1" dimColor>{`SCORE ${score} · PAGE ${block[0]}`}</Text>];
206  }
207
208  const page = (
209    <Box key="page" flexDirection="column" width={width}>
210      {header}
211      {status}
212      <Text key="gap1"> </Text>
213      <Box flexDirection="column" paddingX={isFramed ? 1 : 0}>{body}</Box>
214      <Text key="gap2"> </Text>
215      {info}
216      {footer}
217    </Box>
218  );
219
220  return (
221    <Box flexDirection="column" width={width}>
222      {page}
223      <Box flexDirection="row" flexWrap="wrap" columnGap={2}>
224        <Button key="tab-view" plain hotkey="v" label="view" dimColor={tab !== 'view'} onPress={() => onTab('view')} />
225        <Button key="tab-original" plain hotkey="o" label="original" dimColor={tab !== 'original'} onPress={() => onTab('original')} />
226        <Button key="tab-check" plain hotkey="c" label="check" dimColor={tab !== 'check'} onPress={() => onTab('check')} />
227        <Button key="tab-session" plain hotkey="s" label="session" dimColor={tab !== 'session'} onPress={() => onTab('session')} />
228        {isSession
229          ? <Button key="record" plain hotkey="g" label={record ? 'write again' : 'write record'} dimColor={isRecording} onPress={onRecord} />
230          : <Button key="rewrite" plain hotkey="r" label="rewrite" dimColor={!view || isRewriting} onPress={onRewrite} />}
231        <Button key="close" plain hotkey="x" label="close" role="dismiss" onPress={onClose} />
232      </Box>
233    </Box>
234  );
235}
236
hooks/sheet.jsx 130 lines
1/**
2 * STE check: the parts of the pane's check tab.
3 *
4 *   - the prose of an answer, without code, tables and links
5 *   - sentence lengths against the word limit
6 *   - findings (unapproved words, passive voice, style)
7 */
8
9const RULE_LABELS = {
10  SENTENCE_LENGTH: 'length',
11  PASSIVE_VOICE: 'passive',
12  UNAPPROVED_WORD: 'word',
13  VERB_TENSE: 'verb form',
14  NOUN_CLUSTER: 'noun cluster',
15  PARAGRAPH_LENGTH: 'paragraph',
16};
17
18// Remove the parts of a markdown answer that are not prose: code, tables, links, list marks
19export function proseOf(answer) {
20  return (answer || '')
21    .replace(/```[\s\S]*?```/g, '\n\n')
22    .replace(/`[^`\n]*`/g, 'CODE')
23    .split('\n')
24    .filter(line => !/^\s*\|/.test(line))
25    .map(line => line
26      .replace(/^\s*#{1,6}\s+/, '')
27      .replace(/^\s*(?:[-*+]|\d+[.)])\s+/, '')
28      .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
29      .replace(/https?:\/\/\S+/g, 'URL')
30      .replace(/[*_]{1,2}([^*_]+)[*_]{1,2}/g, '$1'))
31    .join('\n')
32    .trim();
33}
34
35// One row per finding, with repeats of the same word or phrase folded together
36export function findingsOf(report) {
37  const rows = new Map();
38  for (const s of report.sentences) {
39    for (const i of s.issues) {
40      if (i.permitted || i.type === 'SENTENCE_LENGTH') continue;
41      const item = i.word || i.cluster || (i.message.match(/"([^"]+)"/) || [])[1] || i.message;
42      const alt = i.replacement || i.suggestion || (i.type === 'NOUN_CLUSTER' ? i.message : '');
43      const id = `${i.type}:${item.toLowerCase()}`;
44      const row = rows.get(id) || { type: i.type, item, alt, severity: i.severity, count: 0 };
45      row.count += 1;
46      rows.set(id, row);
47    }
48  }
49  const rank = { error: 0, warning: 1, info: 2 };
50  return [...rows.values()].sort((a, b) => rank[a.severity] - rank[b.severity] || b.count - a.count);
51}
52
53// Terminal cells of a character: 2 for Hangul, CJK, full-width forms and emoji, else 1
54const WIDE = /[\u1100-\u115f\u2e80-\u303e\u3041-\u33ff\u3400-\u4dbf\u4e00-\u9fff\ua000-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe30-\ufe4f\uff00-\uff60\uffe0-\uffe6]/;
55const cellsOf = (ch) => (WIDE.test(ch) || ch.codePointAt(0) > 0xffff ? 2 : 1);
56
57// Terminal cells of a text
58export const widthOf = (text) => Array.from(text).reduce((n, ch) => n + cellsOf(ch), 0);
59
60// The text cut to at most n cells, with … when it is cut
61export const clip = (text, n) => {
62  if (widthOf(text) <= n) return text;
63  let out = '';
64  let used = 0;
65  for (const ch of Array.from(text)) {
66    if (used + cellsOf(ch) > n - 1) break;
67    out += ch;
68    used += cellsOf(ch);
69  }
70  return `${out}…`;
71};
72
73// The text filled with spaces to n cells
74export const padTo = (text, n) => text + ' '.repeat(Math.max(0, n - widthOf(text)));
75const pad = (text, n) => padTo(clip(text, n), n);
76
77// The longest sentences as bars, with the limit marked
78export function lengthRows({ h, Text }, report, limitOf, barWidth) {
79  const longest = [...report.sentences].sort((a, b) => b.wordCount - a.wordCount).slice(0, 4)
80    .sort((a, b) => a.index - b.index);
81  const scale = Math.max(30, ...longest.map(s => s.wordCount));
82  return longest.map(s => {
83    const limit = limitOf(s);
84    const filled = Math.round((s.wordCount / scale) * barWidth);
85    const mark = Math.round((limit / scale) * barWidth);
86    const inside = Math.min(filled, mark);
87    const over = Math.max(0, filled - mark);
88    const rest = Math.max(0, barWidth - Math.max(filled, mark));
89    const isOver = s.wordCount > limit;
90    return (
91      <Text key={`s${s.index}`}>
92        <Text dimColor>{pad(`S${s.index}`, 4)}</Text>
93        <Text color="blue">{'█'.repeat(inside)}</Text>
94        <Text dimColor>{'░'.repeat(Math.max(0, mark - inside))}</Text>
95        <Text color={isOver ? 'red' : 'blue'}>│</Text>
96        <Text color="red">{'█'.repeat(over)}</Text>
97        <Text dimColor>{'░'.repeat(rest)}</Text>
98        <Text color={isOver ? 'red' : undefined}>{` ${s.wordCount}/${limit}`}</Text>
99        {isOver ? <Text color="red"> ✕</Text> : null}
100      </Text>
101    );
102  });
103}
104
105// A dictionary-style table of findings
106export function findingRows({ h, Text }, findings, width, limit = 5) {
107  if (findings.length === 0) {
108    return [<Text key="none" color="green">✓ No unapproved words, passive voice or style issues.</Text>];
109  }
110  const itemWidth = Math.min(22, Math.max(10, Math.floor(width * 0.32)));
111  const kindWidth = 12;
112  const altWidth = Math.max(8, width - itemWidth - kindWidth - 6);
113  const shown = findings.slice(0, limit);
114  const rows = [
115    <Text key="head" dimColor>{`  ${pad('Not approved', itemWidth)} ${pad('Rule', kindWidth)} Use instead`}</Text>,
116    ...shown.map((f, n) => (
117      <Text key={`f${n}`}>
118        <Text color={f.severity === 'info' ? 'yellow' : 'red'}>{f.severity === 'info' ? '! ' : '✕ '}</Text>
119        <Text color={f.severity === 'info' ? undefined : 'red'}>{pad(f.item + (f.count > 1 ? ` ×${f.count}` : ''), itemWidth)}</Text>
120        <Text dimColor>{` ${pad(RULE_LABELS[f.type] || f.type, kindWidth)} `}</Text>
121        <Text color="blue">{clip(f.alt, altWidth)}</Text>
122      </Text>
123    )),
124  ];
125  if (findings.length > shown.length) {
126    rows.push(<Text key="more" dimColor>{`  +${findings.length - shown.length} more · /asd check <text> for the full report`}</Text>);
127  }
128  return rows;
129}
130
hooks/view.jsx 363 lines
1/**
2 * STE view: an answer laid out as an ASD-STE100 document.
3 *
4 * - Headings become section titles.
5 * - Descriptive text shows one sentence on each line.
6 * - Numbered lines become procedure steps.
7 * - WARNING, CAUTION and NOTE lines become signal blocks (STE Section 7).
8 * - Unapproved words show in red with the approved word after them.
9 * - A sentence over the word limit shows its word count in red.
10 */
11
12import { validateText } from '../lib/ste-engine.js';
13import { padTo, widthOf } from './sheet.jsx';
14
15// Words that ask for an ASD-STE100 answer
16export const STE_REQUEST = /asd[- ]?ste|ste[- ]?100|simplified technical english|\bSTE\b|karpathy/i;
17
18const SIGNAL = /^\s*(?:\*\*)?(WARNING|CAUTION|NOTE)(?:\*\*)?\s*:?\s*(?:\*\*)?\s*/i;
19const SIGNAL_COLOR = { WARNING: 'red', CAUTION: 'yellow', NOTE: 'blue' };
20
21const plain = (text) => text
22  .replace(/`([^`]+)`/g, '$1')
23  .replace(/\*\*([^*]+)\*\*/g, '$1')
24  .replace(/(^|\W)[*_]([^*_]+)[*_](?=\W|$)/g, '$1$2')
25  .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
26
27/**
28 * Split a markdown answer into STE blocks.
29 * @returns {Array<{kind: string, text?: string, lines?: string[], n?: number, signal?: string}>}
30 */
31export function blocksOf(answer) {
32  const blocks = [];
33  let paragraph = [];
34  let code = null;
35  let lang = '';
36  const flush = () => {
37    if (paragraph.length) blocks.push({ kind: 'text', text: plain(paragraph.join(' ')) });
38    paragraph = [];
39  };
40
41  for (const line of (answer || '').split('\n')) {
42    if (code) {
43      if (/^\s*```/.test(line)) {
44        blocks.push({ kind: 'code', lang, lines: code });
45        code = null;
46      } else {
47        code.push(line);
48      }
49      continue;
50    }
51    if (/^\s*```/.test(line)) {
52      flush();
53      code = [];
54      lang = line.trim().slice(3).trim();
55      continue;
56    }
57    if (!line.trim()) {
58      flush();
59      continue;
60    }
61    const heading = line.match(/^\s*(#{1,6})\s+(.*)$/);
62    if (heading) {
63      flush();
64      blocks.push({ kind: 'title', level: heading[1].length, text: plain(heading[2]) });
65      continue;
66    }
67    if (SIGNAL.test(line)) {
68      flush();
69      const signal = line.match(SIGNAL)[1].toUpperCase();
70      blocks.push({ kind: 'signal', signal, text: plain(line.replace(SIGNAL, '')) });
71      continue;
72    }
73    const step = line.match(/^\s*(\d+)[.)]\s+(.*)$/);
74    if (step) {
75      flush();
76      blocks.push({ kind: 'step', n: Number(step[1]), text: plain(step[2]) });
77      continue;
78    }
79    const item = line.match(/^\s*[-*+]\s+(.*)$/);
80    if (item) {
81      flush();
82      blocks.push({ kind: 'item', text: plain(item[1]) });
83      continue;
84    }
85    if (/^\s*\|/.test(line)) {
86      flush();
87      const last = blocks[blocks.length - 1];
88      if (last && last.kind === 'table') last.lines.push(line.trim());
89      else blocks.push({ kind: 'table', lines: [line.trim()] });
90      continue;
91    }
92    paragraph.push(line.trim());
93  }
94  if (code) blocks.push({ kind: 'code', lang, lines: code });
95  flush();
96  return blocks;
97}
98
99// Words of `text` in lines of at most `width` cells; a longer word is cut
100function wrapCell(text, width) {
101  const lines = [];
102  let line = '';
103  const cut = (word) => {
104    let head = '';
105    for (const ch of Array.from(word)) {
106      if (widthOf(head + ch) > width) break;
107      head += ch;
108    }
109    return head || Array.from(word)[0];
110  };
111  for (let word of text.split(/\s+/).filter(Boolean)) {
112    while (widthOf(word) > width) {
113      if (line) { lines.push(line); line = ''; }
114      const head = cut(word);
115      lines.push(head);
116      word = word.slice(head.length);
117    }
118    if (!line) line = word;
119    else if (widthOf(line) + 1 + widthOf(word) <= width) line += ' ' + word;
120    else { lines.push(line); line = word; }
121  }
122  if (line || lines.length === 0) lines.push(line);
123  return lines;
124}
125
126// Column widths that fit `room` cells: a narrow column keeps its width, a wide one shares the rest
127function fitColumns(natural, room) {
128  const widths = natural.map(() => 0);
129  let left = room;
130  let open = natural.map((_, i) => i);
131  while (open.length) {
132    const share = Math.floor(left / open.length);
133    const small = open.filter(i => natural[i] <= share);
134    if (small.length === 0) {
135      open.forEach((i, n) => { widths[i] = Math.max(3, share + (n < left - share * open.length ? 1 : 0)); });
136      break;
137    }
138    for (const i of small) { widths[i] = natural[i]; left -= natural[i]; }
139    open = open.filter(i => !small.includes(i));
140  }
141  return widths;
142}
143
144// A markdown table drawn as a text grid that fits `width` cells, with wrapped cells and a bold header
145function tableRows(els, key, lines, width) {
146  const { h, Text } = els;
147  const rows = lines
148    .filter(line => !/^\s*\|?[\s:|-]+\|?\s*$/.test(line))
149    .map(line => line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map(c => plain(c.trim())));
150  const n = Math.max(...rows.map(r => r.length));
151  rows.forEach(r => { while (r.length < n) r.push(''); });
152  const natural = Array.from({ length: n }, (_, i) => Math.max(3, ...rows.map(r => widthOf(r[i]))));
153  const widths = fitColumns(natural, Math.max(n * 3, width - (n + 1) - 2 * n));
154  const line = (k, [l, c, r]) => <Text key={k} dimColor>{l + widths.map(w => '─'.repeat(w + 2)).join(c) + r}</Text>;
155  const out = [line(`${key}-top`, ['┌', '┬', '┐'])];
156  rows.forEach((row, ri) => {
157    const cells = row.map((c, i) => wrapCell(c, widths[i]));
158    const height = Math.max(...cells.map(c => c.length));
159    for (let li = 0; li < height; li++) {
160      const parts = [<Text key="b0" dimColor>│</Text>];
161      cells.forEach((c, i) => {
162        const text = padTo(c[li] || '', widths[i]);
163        parts.push(<Text key={`c${i}`} bold={ri === 0} color={ri === 0 ? 'cyan' : undefined}>{` ${text} `}</Text>);
164        parts.push(<Text key={`b${i + 1}`} dimColor>│</Text>);
165      });
166      out.push(<Text key={`${key}-${ri}-${li}`} wrap="truncate-end">{parts}</Text>);
167    }
168    if (ri < rows.length - 1) out.push(line(`${key}-sep${ri}`, ri === 0 ? ['╞', '╪', '╡'] : ['├', '┼', '┤']));
169  });
170  out.push(line(`${key}-bottom`, ['└', '┴', '┘']));
171  return out;
172}
173
174// A code block drawn as markdown the way an assistant reply draws it; a table as a text grid
175function markdownBlock(els, key, b, width = 80) {
176  const { h, Text, Markdown } = els;
177  if (b.kind === 'table') return tableRows(els, key, b.lines, width);
178  const text = b.kind === 'code'
179    ? ['```' + (b.lang || ''), ...b.lines, '```'].join('\n')
180    : b.lines.join('\n');
181  if (!Markdown) return b.lines.map((line, i) => <Text key={`${key}-${i}`} dimColor wrap="truncate-end">{line}</Text>);
182  return [<Markdown key={key} text={text.slice(0, 10000)} />];
183}
184
185// A heading without the number the model wrote, so the page numbers it once ("2. State" -> "State")
186const unnumbered = (name) => name.replace(/^\s*(?:\d+(?:\.\d+)*|[A-Za-z])[.)]\s+/, '');
187
188// One sentence with its unapproved words marked, and its word count when it is too long
189function sentenceLine({ h, Text }, report, key, prefix, limitOf) {
190  const s = report.sentences[0];
191  if (!s) return null;
192  const words = s.issues.filter(i => i.type === 'UNAPPROVED_WORD' && i.word);
193  const parts = [];
194  let rest = s.text;
195  for (const w of words) {
196    const at = rest.toLowerCase().indexOf(w.word.toLowerCase());
197    if (at < 0) continue;
198    parts.push(rest.slice(0, at));
199    parts.push(
200      <Text key={`${key}-${parts.length}`}>
201        <Text color="red" underline>{rest.slice(at, at + w.word.length)}</Text>
202        {w.replacement ? <Text color="blue">{` →${w.replacement.split(/[,(]/)[0].trim().toUpperCase()}`}</Text> : null}
203      </Text>
204    );
205    rest = rest.slice(at + w.word.length);
206  }
207  parts.push(rest);
208  const limit = limitOf(s);
209  return (
210    <Text key={key}>
211      {prefix}
212      {parts}
213      {s.wordCount > limit ? <Text color="red">{`  ${s.wordCount}/${limit} words`}</Text> : null}
214    </Text>
215  );
216}
217
218function sentencesOf(text, mode) {
219  return validateText(text, { mode }).sentences.map(s => ({ ...s, report: { sentences: [s] } }));
220}
221
222/**
223 * Draw an answer as an STE document.
224 * @param {object} els  - the surface's element table plus `h`
225 * @param {string} text - the answer
226 * @param {object} opts - { mode, limitOf, columns }
227 */
228export function viewRows(els, text, { mode, limitOf, columns }) {
229  const { h, Box, Text } = els;
230  const rows = [];
231  let n = 0;
232  const key = () => `v${n++}`;
233
234  for (const b of blocksOf(text)) {
235    if (b.kind === 'title') {
236      if (rows.length) rows.push(<Text key={key()}> </Text>);
237      rows.push(<Text key={key()} bold>{b.text}</Text>);
238      continue;
239    }
240    if (b.kind === 'signal') {
241      rows.push(
242        <Box key={key()} flexDirection="column" borderStyle="single" borderColor={SIGNAL_COLOR[b.signal]} width={Math.max(20, columns)}>
243          <Text bold color={SIGNAL_COLOR[b.signal]}>{b.signal}</Text>
244          {sentencesOf(b.text, mode).map(s => sentenceLine(els, s.report, key(), '', limitOf))}
245        </Box>
246      );
247      continue;
248    }
249    if (b.kind === 'step' || b.kind === 'item') {
250      const mark = b.kind === 'step' ? `${b.n}. ` : '• ';
251      sentencesOf(b.text, mode).forEach((s, i) => {
252        rows.push(sentenceLine(els, s.report, key(), <Text color="blue">{i === 0 ? mark : ' '.repeat(mark.length)}</Text>, limitOf));
253      });
254      continue;
255    }
256    if (b.kind === 'code' || b.kind === 'table') {
257      rows.push(...markdownBlock(els, key(), b, columns));
258      continue;
259    }
260    // Descriptive text: one sentence on each line, then a gap after the paragraph
261    for (const s of sentencesOf(b.text, mode)) rows.push(sentenceLine(els, s.report, key(), '', limitOf));
262    rows.push(<Text key={key()}> </Text>);
263  }
264  return rows.filter(Boolean);
265}
266
267// The document title: the first level-1 heading, or the first heading
268export function titleOf(text) {
269  const titles = blocksOf(text).filter(b => b.kind === 'title');
270  const first = titles.find(b => b.level === 1) || titles[0];
271  return first ? unnumbered(first.text) : '';
272}
273
274const letter = (i) => String.fromCharCode(65 + (i % 26));
275const HANGUL = /[\uac00-\ud7a3]/;
276
277/**
278 * Draw an STE document as a page of a maintenance manual.
279 *   1. SECTION       a heading
280 *      A. Sentence   one sentence of descriptive text
281 *      (1) Step      a numbered procedure step
282 *      (a) Item      a list item
283 * WARNING, CAUTION and NOTE are boxes with the signal word in the middle of the top line.
284 * @param {object} els  - the surface's element table plus `h`
285 * @param {string} text - the STE document
286 * @param {object} opts - { mode, limitOf, columns }
287 */
288export function manualRows(els, text, { mode, limitOf, columns }) {
289  const { h, Box, Text } = els;
290  const rows = [];
291  let n = 0;
292  const key = () => `m${n++}`;
293  const title = titleOf(text);
294  let section = 0;
295  let sentence = 0;
296  let item = 0;
297  let titleSkipped = false;
298
299  const startSection = (name) => {
300    section += 1;
301    sentence = 0;
302    item = 0;
303    if (rows.length) rows.push(<Text key={key()}> </Text>);
304    rows.push(<Text key={key()} bold>{`${section}. ${unnumbered(name).toUpperCase()}`}</Text>);
305  };
306  // A numbered line whose wrapped lines start under the text, not under the number
307  const numbered = (indent, mark, body, color) => (
308    <Box key={key()} flexDirection="row">
309      <Box width={indent + mark.length} flexShrink={0}>
310        <Text color={color}>{' '.repeat(indent) + mark}</Text>
311      </Box>
312      <Box flexGrow={1} flexShrink={1}>{body}</Box>
313    </Box>
314  );
315
316  for (const b of blocksOf(text)) {
317    if (b.kind === 'title') {
318      if (!titleSkipped && unnumbered(b.text) === title) {
319        titleSkipped = true;
320        continue;
321      }
322      startSection(b.text);
323      continue;
324    }
325    if (section === 0 && b.kind !== 'code' && b.kind !== 'table') startSection(HANGUL.test(text) ? '개요' : 'General');
326    if (b.kind === 'signal') {
327      const color = SIGNAL_COLOR[b.signal];
328      rows.push(
329        <Box key={key()} flexDirection="column" borderStyle="single" borderColor={color} width={Math.max(20, columns)} paddingX={1}>
330          <Box justifyContent="center"><Text bold inverse color={color}>{` ${b.signal} `}</Text></Box>
331          {sentencesOf(b.text, mode).map(s => sentenceLine(els, s.report, key(), '', limitOf))}
332        </Box>
333      );
334      continue;
335    }
336    if (b.kind === 'step') {
337      item = 0;
338      sentencesOf(b.text, mode).forEach((s, i) => {
339        const mark = i === 0 ? `(${b.n}) ` : ' '.repeat(`(${b.n}) `.length);
340        rows.push(numbered(3, mark, sentenceLine(els, s.report, key(), '', limitOf), 'blue'));
341      });
342      continue;
343    }
344    if (b.kind === 'item') {
345      const mark = `(${String.fromCharCode(97 + (item % 26))}) `;
346      item += 1;
347      rows.push(numbered(3, mark, sentenceLine(els, { sentences: sentencesOf(b.text, mode).slice(0, 1) }, key(), '', limitOf), 'blue'));
348      for (const s of sentencesOf(b.text, mode).slice(1)) rows.push(numbered(3, ' '.repeat(mark.length), sentenceLine(els, s.report, key(), '', limitOf)));
349      continue;
350    }
351    if (b.kind === 'code' || b.kind === 'table') {
352      rows.push(<Box key={key()} flexDirection="column" paddingLeft={3}>{markdownBlock(els, key(), b, columns - 3)}</Box>);
353      continue;
354    }
355    item = 0;
356    for (const s of sentencesOf(b.text, mode)) {
357      rows.push(numbered(3, `${letter(sentence)}. `, sentenceLine(els, s.report, key(), '', limitOf)));
358      sentence += 1;
359    }
360  }
361  return rows.filter(Boolean);
362}
363
types/index.d.ts 22 lines
1export type SteIssue = { type: string; severity: string; message: string; permitted?: boolean; word?: string; cluster?: string; replacement?: string; suggestion?: string }
2export type SteSentence = { index: number; text: string; wordCount: number; isProcedural: boolean; issues: SteIssue[] }
3export type SteReport = { mode: string; score: number; totalSentences: number; totalWords: number; averageWordsPerSentence: number; totalIssues: number; sentences: SteSentence[] }
4export type SteRecord = { text: string; source: 'fork' | 'lite'; date: string; rev: number }
5export type SteView = { text: string; original: string; source: 'answer' | 'rewrite'; isAsked: boolean; task: number; rev: number; date: string }
6
7declare module 'claude-code' {
8  interface PluginState {
9    'asd-ste100': {
10      view: SteView | null
11      report: SteReport | null
12      tab: string
13      isAsked: boolean
14      isRewriting: boolean
15      paneOpen: boolean
16      record: SteRecord | null
17      recordReport: SteReport | null
18      isRecording: boolean
19    }
20  }
21}
22