diff --git a/contracts/animTokens.js b/contracts/animTokens.js deleted file mode 100644 index 73ee97a..0000000 --- a/contracts/animTokens.js +++ /dev/null @@ -1,44 +0,0 @@ -/*╔══════════════════════════════════════════════════════╗ - ║ ░ A N I M T O K E N S ( J S ) ░░░░░░░░░░░░░░ ║ - ║ ║ - ║ ║ - ║ ║ - ║ ║ - ║ ╌╌ P L A C E H O L D E R ╌╌ ║ - ║ ║ - ║ ║ - ║ ║ - ║ ║ - ╚══════════════════════════════════════════════════════╝ - • WHAT ▸ Browser JS tokens for demos (mirror of TS) - • WHY ▸ CONTRACT-BAND-SWAP - • HOW ▸ Imported by demo ESM modules -*/ - -export const DEFAULT_SYMBOLS = [ - '\u2800', - '\u2802', - '\u2804', - '\u2806', - '\u2810', - '\u2812', - '\u2814', - '\u2816', - '\u2820', - '\u2822', - '\u2824', - '\u2826', - '\u2830', - '\u2832', - '\u2834', - '\u2836', -]; - -export const DEFAULT_TOKENS = { - bandSpeed: 0.5, - bandSpread: 5, - bandMix: 50, - symbolSet: DEFAULT_SYMBOLS, - autoplay: true, - playhead: 0, -}; diff --git a/core/diffusionController.ts b/core/diffusionController.ts index 0c28230..131a33b 100644 --- a/core/diffusionController.ts +++ b/core/diffusionController.ts @@ -47,6 +47,12 @@ export function createDiffusionController( policy?: ActiveRegionPolicy, getLMAdapter?: () => LMAdapter | null | undefined, ) { + // ⟢ Orchestrates the three-stage pipeline: + // 1) Noise: cheap, deterministic fixes within a short window behind caret + // 2) Context: sentence-scale repairs; may plan LM corrections + // 3) Tone: optional rephrasing within caret-safe pre-caret range + // The controller also renders the active region and applies LM streaming diffs + // with strict caret-safety. UndoIsolation groups system edits separately. // Safari/older browsers: Intl.Segmenter may be missing or partial. Provide a fallback. let seg: Intl.Segmenter | null = null; try { diff --git a/core/lm/types.generated.ts b/core/lm/types.generated.ts index 12596d6..d8a4125 100644 --- a/core/lm/types.generated.ts +++ b/core/lm/types.generated.ts @@ -1,5 +1,12 @@ /* Auto-generated by doc2code — do not edit by hand */ +export interface LMStreamParams { + text: string; + caret: number; + active_region: { start: number; end: number }; + settings?: Record; +} + export interface AnimTokens { waveSpeed: number; waveSpread: number; @@ -13,10 +20,3 @@ export const DEFAULT_SYMBOLS = [ '\u2800','\u2802','\u2804','\u2806','\u2810','\u2812','\u2814','\u2816', '\u2820','\u2822','\u2824','\u2826','\u2830','\u2832','\u2834','\u2836', ] as const; - -export interface LMStreamParams { - text: string; - caret: number; - active_region: { start: number; end: number }; - settings?: Record; -} diff --git a/demo/mt-braille-animation-v1/index.html b/demo/mt-braille-animation-v1/index.html index 3f5a7b5..ee150e3 100644 --- a/demo/mt-braille-animation-v1/index.html +++ b/demo/mt-braille-animation-v1/index.html @@ -4,7 +4,7 @@ Mind Type — Braille Flicker - + diff --git a/docs/traceability.json b/docs/traceability.json index 8ba025c..c507df2 100644 --- a/docs/traceability.json +++ b/docs/traceability.json @@ -198,35 +198,6 @@ "types": [], "source": "docs/02-implementation/02-Implementation.md" }, - "CONTRACT-DOT-MATRIX-WAVE": { - "kind": "CONTRACT", - "title": "Dot-matrix wave animation tokens", - "modules": [ - "contracts/animTokens.ts", - "demo/dot-matrix-wave/main.js" - ], - "acceptance": [], - "tests": [], - "invariants": [ - { - "Preserve layout": "no per-char DOM mutations; overlay only" - }, - { - "Reduced-motion": "static correction highlight, no rAF" - } - ], - "types": [ - { - "name": "AnimTokens", - "ts": "export interface AnimTokens {\n waveSpeed: number;\n waveSpread: number;\n waveMix: number; // 0..100\n symbolSet: string[];\n autoplay: boolean;\n playhead: number; // 0..100\n}\n" - }, - { - "name": "DEFAULT_SYMBOLS", - "ts": "export const DEFAULT_SYMBOLS = [\n '\\u2800','\\u2802','\\u2804','\\u2806','\\u2810','\\u2812','\\u2814','\\u2816',\n '\\u2820','\\u2822','\\u2824','\\u2826','\\u2830','\\u2832','\\u2834','\\u2836',\n] as const;\n" - } - ], - "source": "docs/06-guides/dot-matrix-wave.md" - }, "CONTRACT-ACTIVE-REGION": { "kind": "CONTRACT", "title": "Active region policy (render vs context ranges)", @@ -258,7 +229,7 @@ "types": [ { "name": "LMStreamParams", - "ts": "export interface LMStreamParams {\n text: string;\n caret: number;\n active_region: { start: number; end: number };\n settings?: Record;\n}\n" + "ts": "export interface LMStreamParams {\n text: string;\n caret: number;\n active_region: { start: number; end: number };\n settings?: Record;\n}" } ], "source": "docs/06-guides/06-03-reference/lm-behavior.md" @@ -282,5 +253,30 @@ ], "types": [], "source": "docs/06-guides/06-03-reference/lm-stream.md" + }, + "CONTRACT-DOT-MATRIX-WAVE": { + "kind": "CONTRACT", + "title": "Dot-matrix wave animation tokens", + "modules": [ + "contracts/animTokens.ts", + "demo/dot-matrix-wave/main.js" + ], + "acceptance": [], + "tests": [], + "invariants": [ + "Preserve layout: no per-char DOM mutations; overlay only", + "Reduced-motion: static correction highlight, no rAF" + ], + "types": [ + { + "name": "AnimTokens", + "ts": "export interface AnimTokens {\n waveSpeed: number;\n waveSpread: number;\n waveMix: number; // 0..100\n symbolSet: string[];\n autoplay: boolean;\n playhead: number; // 0..100\n}" + }, + { + "name": "DEFAULT_SYMBOLS", + "ts": "export const DEFAULT_SYMBOLS = [\n '\\u2800','\\u2802','\\u2804','\\u2806','\\u2810','\\u2812','\\u2814','\\u2816',\n '\\u2820','\\u2822','\\u2824','\\u2826','\\u2830','\\u2832','\\u2834','\\u2836',\n] as const;" + } + ], + "source": "docs/06-guides/dot-matrix-wave.md" } } \ No newline at end of file diff --git a/engines/contextTransformer.ts b/engines/contextTransformer.ts index 0442640..6dba707 100644 --- a/engines/contextTransformer.ts +++ b/engines/contextTransformer.ts @@ -118,6 +118,10 @@ export async function contextTransform( lmAdapter?: LMAdapter, contextManager?: LMContextManager, ): Promise { + // ⟢ Builds a sentence-aware window and proposes caret-safe diffs. + // - Deterministic repairs first (cheap, predictable) + // - Optional LM pass uses policy-driven band selection and confidence gating + // - Never edits at/after caret; proposals are clamped to pre-caret span const { text, caret } = input; // Enhanced diagnostic logging for LM-501 const diagnosticId = `ctx-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`; diff --git a/engines/noiseTransformer.ts b/engines/noiseTransformer.ts index f06b036..8c2d4bb 100644 --- a/engines/noiseTransformer.ts +++ b/engines/noiseTransformer.ts @@ -330,6 +330,9 @@ const RULES: NoiseRule[] = [ export function noiseTransform(input: NoiseInput): NoiseResult { const { text, caret } = input; + // ⟢ Fast path: run a small set of rules in priority order and return + // the rightmost safe diff behind the caret. This keeps the UI snappy + // and avoids changing text near the user's current typing position. console.log('[Noise] Processing:', { text, caret }); // Safety check: never edit at or after the caret diff --git a/engines/toneTransformer.ts b/engines/toneTransformer.ts index a96ab2a..67f96fb 100644 --- a/engines/toneTransformer.ts +++ b/engines/toneTransformer.ts @@ -57,6 +57,9 @@ export function planAdjustments( text: string, caret: number, ): ToneProposal[] { + // ⟢ Produces caret-safe style adjustments (pre-caret only). + // Professional → expand contractions; Casual → introduce a few. + // Keeps changes minimal and explainable; LM can later refine phrasing. if (target === 'None') return []; // Operate on last sentences before the caret only (caret-safe) const upto = caret; diff --git a/scripts/doc2code.cjs b/scripts/doc2code.cjs index ac59eb2..fa8213c 100644 --- a/scripts/doc2code.cjs +++ b/scripts/doc2code.cjs @@ -20,8 +20,7 @@ const fs = require('fs'); const path = require('path'); // ⟢ Deps -const fg = require('fast-glob'); -const yaml = require('js-yaml'); +// NOTE: Avoid external deps to keep scripts runnable without install const REPO_ROOT = path.resolve(__dirname, '..'); const DOCS_DIR = path.join(REPO_ROOT, 'docs'); @@ -40,7 +39,7 @@ function extractSpecBlocks(markdownText, filePath) { const kind = m[1].trim(); const body = m[2]; try { - const data = yaml.load(body); + const data = parseSpecYaml(body); if (!data || typeof data !== 'object') continue; data.__kind = kind; data.__source = filePath; @@ -55,11 +54,152 @@ function extractSpecBlocks(markdownText, filePath) { return blocks; } +/** + * Minimal YAML parser for SPEC blocks. Supports a safe subset: + * - key: value (scalars) + * - key: (array) with indented dash items (- item) + * - key: (array of objects) started with '- name:' style + * - special handling for 'ts: |' multiline blocks indented under a list item + */ +function parseSpecYaml(body) { + const lines = body + .replace(/^[\r\n]+|[\r\n]+$/g, '') + .split(/\r?\n/); + let i = 0; + const root = {}; + let currentKey = null; + let currentArray = null; + let inMultiline = false; + let multilineIndent = 0; + let multilineTarget = null; // {obj, key} + + function indentOf(s) { + let n = 0; + while (n < s.length && s[n] === ' ') n++; + return n; + } + + function setScalar(obj, key, value) { + if (value === 'true') return (obj[key] = true); + if (value === 'false') return (obj[key] = false); + if (value === 'null') return (obj[key] = null); + // number? + if (/^-?\d+(?:\.\d+)?$/.test(value)) return (obj[key] = Number(value)); + return (obj[key] = value); + } + + while (i < lines.length) { + const raw = lines[i]; + const line = raw.replace(/\t/g, ' '); + i++; + if (!line.trim()) continue; + + if (inMultiline) { + const ind = indentOf(line); + if (ind < multilineIndent) { + // end block + inMultiline = false; + multilineIndent = 0; + multilineTarget = null; + // fallthrough to normal processing of this line + } else { + const text = line.slice(multilineIndent); + multilineTarget.obj[multilineTarget.key] += (multilineTarget.obj[multilineTarget.key] + ? '\n' + : '') + text; + continue; + } + } + + const ind = indentOf(line); + const trimmed = line.slice(ind); + + // Top-level key + if (ind === 0 && /:\s*/.test(trimmed) && !trimmed.startsWith('- ')) { + const [k, rest] = trimmed.split(/:\s*/, 2); + currentKey = k.trim(); + if (rest === '') { + // key: (will expect list or map) + root[currentKey] = undefined; + currentArray = null; + } else { + setScalar(root, currentKey, rest.trim()); + currentArray = null; + } + continue; + } + + // Array items under currentKey + if (currentKey && ind > 0 && trimmed.startsWith('- ')) { + if (!Array.isArray(root[currentKey])) root[currentKey] = []; + currentArray = root[currentKey]; + const afterDash = trimmed.slice(2); + // Object item like: "name: Foo" or scalar item + if (/^\w+\s*:/.test(afterDash)) { + const obj = {}; + currentArray.push(obj); + // parse in-line first k:v + const [k, rest] = afterDash.split(/:\s*/, 2); + setScalar(obj, k.trim(), (rest || '').trim()); + // Now parse following indented object lines + const baseIndent = ind + 2; // indent after '- ' + while (i < lines.length) { + const peekRaw = lines[i]; + const peek = peekRaw.replace(/\t/g, ' '); + const pind = indentOf(peek); + if (pind <= ind) break; + i++; + const ptrim = peek.slice(pind); + if (!/:\s*/.test(ptrim)) continue; + const [pk, prest] = ptrim.split(/:\s*/, 2); + const key = pk.trim(); + const val = (prest || '').trim(); + if (val === '|') { + // multiline block begins; consume subsequent lines with greater indent + obj[key] = ''; + inMultiline = true; + multilineIndent = pind + 2; // expect additional indent for block content + multilineTarget = { obj, key }; + break; + } else { + setScalar(obj, key, val); + } + } + } else { + // Scalar list item + currentArray.push(afterDash.trim()); + } + continue; + } + } + + return root; +} + function readAllSpecs() { - const files = fg.sync(['docs/**/*.md'], { cwd: REPO_ROOT, dot: false }); + const docsRoot = path.join(REPO_ROOT, 'docs'); + /** + * Recursively collect markdown files under docs/ using only fs APIs. + */ + function walkMarkdownFiles(dirAbs, out) { + const entries = fs.readdirSync(dirAbs, { withFileTypes: true }); + for (const entry of entries) { + if (entry.name.startsWith('.')) continue; // skip dotfiles + const childAbs = path.join(dirAbs, entry.name); + if (entry.isDirectory()) { + walkMarkdownFiles(childAbs, out); + } else if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) { + out.push(childAbs); + } + } + } + const fileAbsPaths = []; + if (fs.existsSync(docsRoot)) { + walkMarkdownFiles(docsRoot, fileAbsPaths); + } const specs = []; - for (const rel of files) { - const abs = path.join(REPO_ROOT, rel); + for (const abs of fileAbsPaths) { + const rel = path.relative(REPO_ROOT, abs).split(path.sep).join('/'); const md = fs.readFileSync(abs, 'utf8'); const blocks = extractSpecBlocks(md, rel); specs.push(...blocks); diff --git a/ui/highlighter.ts b/ui/highlighter.ts index e9b28d3..11b670f 100644 --- a/ui/highlighter.ts +++ b/ui/highlighter.ts @@ -23,6 +23,7 @@ interface MinimalGlobal { } export function renderHighlight(_range: { start: number; end: number; text?: string }) { + // ⟢ Host-agnostic event to visualize applied changes (e.g., underline, flash) const g = globalThis as unknown as MinimalGlobal; if (g.dispatchEvent && g.CustomEvent) { const event = new g.CustomEvent('mindtype:highlight', { diff --git a/web-demo/public/_shared/_shared/header.css b/web-demo/public/_shared/_shared/header.css deleted file mode 100644 index 6769c1c..0000000 --- a/web-demo/public/_shared/_shared/header.css +++ /dev/null @@ -1,115 +0,0 @@ -/*╔══════════════════════════════════════════════════════╗ - ║ ░ D E M O H E A D E R ( C S S ) ░░░░░░░░░░░░ ║ - ║ ║ - ║ ║ - ║ ║ - ║ ║ - ║ ╌╌ P L A C E H O L D E R ╌╌ ║ - ║ ║ - ║ ║ - ║ ║ - ║ ║ - ╚══════════════════════════════════════════════════════╝ - • WHAT ▸ Shared responsive header for demo pages - • WHY ▸ Consistent branding and metadata across demos - • HOW ▸ Lightweight CSS variables; JS injects DOM at runtime -*/ - -:root { - --mt-h-bg: #ffffff; - --mt-h-fg: #111111; - --mt-h-muted: #666666; - --mt-h-line: #eaeaea; - --mt-h-pad-x: 16px; - --mt-h-pad-y: 10px; - --mt-h-max: 1320px; -} - -/* Header root */ -.mt-header { - position: sticky; - top: 0; - z-index: 1000; - background: var(--mt-h-bg); - color: var(--mt-h-fg); - border-bottom: 1px solid var(--mt-h-line); -} -.mt-header-inner { - margin: 0 auto; - max-width: var(--mt-h-max); - padding: var(--mt-h-pad-y) var(--mt-h-pad-x); - display: grid; - grid-template-columns: 1fr auto; - gap: 12px; - align-items: center; -} - -/* Brand and title cluster */ -.mt-header-left { - display: flex; - align-items: baseline; - gap: 10px; - flex-wrap: wrap; - min-width: 0; -} -.mt-brand { - font-weight: 700; - letter-spacing: 0.02em; - white-space: nowrap; -} -.mt-brand a { - color: inherit; - text-decoration: none; -} -.title-wrapper { - font-size: 14px; - color: var(--mt-h-muted); - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -/* Meta on the right */ -.mt-header-right { - display: flex; - gap: 12px; - align-items: center; - flex-wrap: wrap; - justify-content: flex-end; -} -.mt-badge { - display: inline-flex; - gap: 6px; - align-items: center; - font-size: 12px; - color: #333; - border: 1px solid var(--mt-h-line); - padding: 4px 8px; - border-radius: 999px; - background: #fff; -} -.mt-badge code { - font-family: - ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', - 'Courier New', monospace; - font-size: 12px; -} - -@media (max-width: 640px) { - .mt-header-inner { - grid-template-columns: 1fr; - } - .mt-header-right { - justify-content: flex-start; - } -} - -@media (prefers-reduced-motion: reduce) { - * { - animation-duration: 0.001ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.001ms !important; - scroll-behavior: auto !important; - } -} diff --git a/web-demo/public/_shared/_shared/header.js b/web-demo/public/_shared/_shared/header.js deleted file mode 100644 index 7cefc62..0000000 --- a/web-demo/public/_shared/_shared/header.js +++ /dev/null @@ -1,83 +0,0 @@ -/*╔══════════════════════════════════════════════════════╗ - ║ ░ D E M O H E A D E R ( J S ) ░░░░░░░░░░░░░░░ ║ - ║ ║ - ║ ║ - ║ ║ - ║ ║ - ║ ╌╌ P L A C E H O L D E R ╌╌ ║ - ║ ║ - ║ ║ - ║ ║ - ║ ║ - ╚══════════════════════════════════════════════════════╝ - • WHAT ▸ Injects a responsive header with title, version and last-updated - • WHY ▸ Keep demos consistent and informative without manual duplication - • HOW ▸ Mounts on DOMContentLoaded; derives title from or URL -*/ - -(function () { - try { - if (document.querySelector('.mt-header')) return; - - const version = - document.querySelector('meta[name="mt-version"]')?.getAttribute('content') || - '0.0.0'; - const updated = - document.querySelector('meta[name="mt-last-updated"]')?.getAttribute('content') || - document.lastModified || - new Date().toISOString(); - - const titleTag = document.querySelector('title'); - const pageTitle = ( - titleTag?.textContent || - document.querySelector('[data-demo-title]')?.textContent || - document.body.getAttribute('data-demo-title') || - location.pathname.split('/').filter(Boolean).slice(-2).join(' / ') || - 'Demo' - ).trim(); - - const header = document.createElement('header'); - header.className = 'mt-header'; - header.innerHTML = [ - '<div class="mt-header-inner">', - '<div class="mt-header-left">', - '<div class="mt-brand"><a href="/">Mind⠶Type</a></div>', - '<div class="title-wrapper" aria-live="polite"></div>', - '</div>', - '<div class="mt-header-right">', - '<span class="mt-badge" title="Project version"><span>v</span><code>' + - escapeHtml(version) + - '</code></span>', - '<span class="mt-badge" title="Last updated"><span>Updated</span><code>' + - escapeHtml(formatLocal(updated)) + - '</code></span>', - '</div>', - '</div>', - ].join(''); - - document.body.prepend(header); - - const titleWrap = header.querySelector('.title-wrapper'); - if (titleWrap) { - titleWrap.textContent = pageTitle; - } - - function formatLocal(iso) { - const d = new Date(iso); - if (Number.isNaN(d.getTime())) return iso; - return d.toLocaleString(); - } - - function escapeHtml(str) { - return String(str) - .replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - } - } catch (err) { - // ⟢ Non-fatal: header injection should never break the demo - console?.warn?.('[mt-header] failed to inject header', err); - } -})();