From 0a27bc73fd412bc81fd87dfbadc3bcbea467f68a Mon Sep 17 00:00:00 2001
From: GangGreenTemperTatum
<104169244+GangGreenTemperTatum@users.noreply.github.com>
Date: Sun, 5 Apr 2026 15:23:09 -0400
Subject: [PATCH 1/2] Add Latin-root lexeme analysis to prompt tools
---
css/style.css | 138 +++++++++++++++++++++++-
index.template.html | 2 +
js/core/lexemeAnalysis.js | 180 ++++++++++++++++++++++++++++++++
js/data/latinAffixPolicies.js | 87 +++++++++++++++
js/tools/AntiClassifierTool.js | 41 ++++++++
js/tools/PromptCraftTool.js | 41 ++++++++
package.json | 4 +-
templates/anticlassifier.html | 42 ++++++++
templates/promptcraft.html | 42 ++++++++
tests/test_lexeme_analysis.js | 40 +++++++
tests/test_lexeme_ui_surface.js | 23 ++++
11 files changed, 638 insertions(+), 2 deletions(-)
create mode 100644 js/core/lexemeAnalysis.js
create mode 100644 js/data/latinAffixPolicies.js
create mode 100644 tests/test_lexeme_analysis.js
create mode 100644 tests/test_lexeme_ui_surface.js
diff --git a/css/style.css b/css/style.css
index c7c92e8..174c914 100644
--- a/css/style.css
+++ b/css/style.css
@@ -3946,6 +3946,143 @@ html {
text-underline-offset: 2px;
}
+.lexeme-analysis-card {
+ margin: 0 0 14px 0;
+ padding: 14px;
+ border: 1px solid var(--input-border);
+ border-radius: 8px;
+ background:
+ linear-gradient(135deg, rgba(25, 118, 210, 0.08), rgba(102, 187, 106, 0.06)),
+ var(--main-bg-color);
+ box-shadow: 0 4px 14px rgba(0,0,0,0.08);
+}
+
+.lexeme-analysis-header {
+ display: flex;
+ gap: 16px;
+ align-items: flex-start;
+ justify-content: space-between;
+ margin-bottom: 12px;
+}
+
+.lexeme-analysis-header h4 {
+ margin: 2px 0 6px 0;
+ font-size: 1rem;
+}
+
+.lexeme-analysis-header p {
+ margin: 0;
+ color: var(--text-muted);
+ line-height: 1.5;
+}
+
+.lexeme-analysis-kicker {
+ font-size: 0.72rem;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--accent-color);
+}
+
+.lexeme-neutralize-btn {
+ align-self: center;
+}
+
+.lexeme-analysis-list {
+ display: grid;
+ gap: 10px;
+}
+
+.lexeme-analysis-item {
+ border: 1px solid var(--input-border);
+ border-radius: 8px;
+ background: rgba(255,255,255,0.03);
+ padding: 12px;
+}
+
+.lexeme-analysis-item-top {
+ display: flex;
+ gap: 12px;
+ align-items: flex-start;
+ justify-content: space-between;
+ margin-bottom: 8px;
+}
+
+.lexeme-analysis-term-group {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ align-items: center;
+}
+
+.lexeme-term {
+ font-size: 0.95rem;
+ font-weight: 700;
+}
+
+.lexeme-chip {
+ display: inline-flex;
+ align-items: center;
+ padding: 2px 8px;
+ border-radius: 999px;
+ border: 1px solid var(--input-border);
+ background: var(--secondary-bg);
+ color: var(--text-muted);
+ font-size: 0.74rem;
+ line-height: 1.4;
+}
+
+.lexeme-analysis-domain {
+ color: var(--text-muted);
+ font-size: 0.82rem;
+ text-align: right;
+}
+
+.lexeme-analysis-rationale {
+ margin: 0 0 10px 0;
+ line-height: 1.45;
+ color: var(--text-color);
+}
+
+.lexeme-analysis-rewrites {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.lexeme-rewrite-btn {
+ padding: 7px 10px;
+ border-radius: 999px;
+ border: 1px solid rgba(25, 118, 210, 0.35);
+ background: rgba(25, 118, 210, 0.08);
+ color: var(--text-color);
+ font: inherit;
+ font-size: 0.82rem;
+ cursor: pointer;
+ transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
+}
+
+.lexeme-rewrite-btn:hover {
+ transform: translateY(-1px);
+ background: rgba(25, 118, 210, 0.14);
+ box-shadow: 0 4px 10px rgba(25, 118, 210, 0.14);
+}
+
+@media (max-width: 720px) {
+ .lexeme-analysis-header,
+ .lexeme-analysis-item-top {
+ flex-direction: column;
+ }
+
+ .lexeme-analysis-domain {
+ text-align: left;
+ }
+
+ .lexeme-neutralize-btn {
+ width: 100%;
+ }
+}
+
.bijection-section .bij-options.options-grid {
margin-top: 0;
margin-bottom: 8px;
@@ -5139,4 +5276,3 @@ html {
margin-left: var(--spacing-md);
}
}
-
diff --git a/index.template.html b/index.template.html
index 9f820ed..004bbaa 100644
--- a/index.template.html
+++ b/index.template.html
@@ -410,6 +410,7 @@
+
@@ -426,6 +427,7 @@
+
diff --git a/js/core/lexemeAnalysis.js b/js/core/lexemeAnalysis.js
new file mode 100644
index 0000000..337e0f1
--- /dev/null
+++ b/js/core/lexemeAnalysis.js
@@ -0,0 +1,180 @@
+/**
+ * Shared Latin-root lexeme analysis and neutral rewrite generation.
+ */
+(function(root) {
+ function uniq(values) {
+ return Array.from(new Set(values.filter(Boolean)));
+ }
+
+ function titleCase(text) {
+ return String(text || '').replace(/\b[a-z]/g, function(match) {
+ return match.toUpperCase();
+ });
+ }
+
+ function preserveCase(source, replacement) {
+ if (!source) {
+ return replacement;
+ }
+ if (source === source.toUpperCase()) {
+ return replacement.toUpperCase();
+ }
+ if (source[0] === source[0].toUpperCase() && source.slice(1) === source.slice(1).toLowerCase()) {
+ return titleCase(replacement);
+ }
+ return replacement;
+ }
+
+ function normalizeRoot(rawRoot, aliases) {
+ const rootText = String(rawRoot || '').toLowerCase();
+ if (!rootText) {
+ return '';
+ }
+
+ if (aliases[rootText]) {
+ return aliases[rootText];
+ }
+
+ const variants = [
+ rootText.endsWith('i') ? rootText.slice(0, -1) : '',
+ rootText.endsWith('ic') ? rootText.slice(0, -2) : '',
+ rootText.endsWith('o') ? rootText.slice(0, -1) : '',
+ rootText.endsWith('al') ? rootText.slice(0, -2) : ''
+ ].filter(Boolean);
+
+ for (const variant of variants) {
+ if (aliases[variant]) {
+ return aliases[variant];
+ }
+ }
+
+ return '';
+ }
+
+ function materializeTemplates(templates, domain) {
+ return uniq((templates || []).map(function(template) {
+ return String(template || '').replace(/\{domain\}/g, domain || 'risk');
+ }));
+ }
+
+ function resolveSuggestions(policy, domain) {
+ if (domain) {
+ return materializeTemplates(policy.rewriteTemplates, domain);
+ }
+ return uniq(policy.fallbackTemplates || []);
+ }
+
+ function analyze(text, options) {
+ const input = String(text || '');
+ const policies = (options && options.policies) || root.LATIN_AFFIX_POLICIES || [];
+ const aliases = (options && options.aliases) || root.LATIN_DOMAIN_ALIASES || {};
+
+ if (!input.trim()) {
+ return {
+ sourceText: input,
+ totalFindings: 0,
+ findings: [],
+ families: [],
+ summary: 'No Latin-root wording findings.'
+ };
+ }
+
+ const findings = [];
+ const seen = new Set();
+
+ policies.forEach(function(policy) {
+ (policy.patterns || []).forEach(function(pattern) {
+ let match;
+ while ((match = pattern.exec(input)) !== null) {
+ const term = match[0];
+ const key = policy.id + '::' + term.toLowerCase();
+ if (seen.has(key)) {
+ continue;
+ }
+ seen.add(key);
+
+ const rootCandidate = match[1] || '';
+ const semanticDomain = normalizeRoot(rootCandidate, aliases);
+ const rewrites = resolveSuggestions(policy, semanticDomain);
+
+ findings.push({
+ id: key,
+ term,
+ normalizedTerm: term.toLowerCase(),
+ family: policy.family,
+ policyId: policy.id,
+ affixes: policy.affixes || [],
+ partOfSpeech: policy.partOfSpeech,
+ extractedRoot: rootCandidate || '',
+ semanticDomain: semanticDomain || '',
+ severity: policy.severity,
+ confidence: policy.confidence,
+ semanticShift: policy.semanticShift,
+ rationale: policy.explanation,
+ rewrites,
+ primaryRewrite: rewrites[0] || '',
+ matchIndex: match.index
+ });
+ }
+ pattern.lastIndex = 0;
+ });
+ });
+
+ findings.sort(function(a, b) {
+ return a.matchIndex - b.matchIndex;
+ });
+
+ return {
+ sourceText: input,
+ totalFindings: findings.length,
+ findings,
+ families: uniq(findings.map(function(item) { return item.family; })),
+ summary: findings.length
+ ? 'Detected ' + findings.length + ' Latin-root wording pattern' + (findings.length === 1 ? '' : 's') + '.'
+ : 'No Latin-root wording findings.'
+ };
+ }
+
+ function neutralizeText(text, analysis) {
+ const input = String(text || '');
+ if (!analysis || !Array.isArray(analysis.findings) || !analysis.findings.length) {
+ return input;
+ }
+
+ let output = input;
+ analysis.findings.forEach(function(finding) {
+ if (!finding.primaryRewrite) {
+ return;
+ }
+ const matcher = new RegExp('\\b' + escapeRegExp(finding.term) + '\\b', 'gi');
+ output = output.replace(matcher, function(match) {
+ return preserveCase(match, finding.primaryRewrite);
+ });
+ });
+ return output;
+ }
+
+ function escapeRegExp(value) {
+ return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ }
+
+ const api = {
+ analyze,
+ neutralizeText
+ };
+
+ if (typeof module !== 'undefined' && module.exports) {
+ const data = require('../data/latinAffixPolicies.js');
+ module.exports = {
+ analyze: function(text) {
+ return analyze(text, {
+ policies: data.POLICIES,
+ aliases: data.DOMAIN_ALIASES
+ });
+ },
+ neutralizeText
+ };
+ } else {
+ root.LexemeAnalysis = api;
+ }
+})(typeof window !== 'undefined' ? window : globalThis);
diff --git a/js/data/latinAffixPolicies.js b/js/data/latinAffixPolicies.js
new file mode 100644
index 0000000..fed4f51
--- /dev/null
+++ b/js/data/latinAffixPolicies.js
@@ -0,0 +1,87 @@
+/**
+ * Declarative Latin-root prompt wording policies.
+ * Families define how loaded affixes should be interpreted and rewritten.
+ */
+(function(root) {
+ const DOMAIN_ALIASES = {
+ bacter: 'bacterial',
+ bacteri: 'bacterial',
+ bio: 'biological',
+ bi: 'biological',
+ fung: 'fungal',
+ fungi: 'fungal',
+ germ: 'microbial',
+ herb: 'weed',
+ homic: 'violence',
+ human: 'human',
+ insect: 'pest',
+ larv: 'larval',
+ pestic: 'pest',
+ pesti: 'pest',
+ rodent: 'rodent',
+ suic: 'self-harm',
+ viru: 'viral',
+ virus: 'viral'
+ };
+
+ const POLICIES = [
+ {
+ id: 'latin-destructive-noun',
+ family: 'destructive_suffix',
+ kind: 'suffix',
+ partOfSpeech: 'noun',
+ affixes: ['cide'],
+ patterns: [/\b([a-z]+?)cide(s)?\b/gi],
+ severity: 'high',
+ confidence: 0.91,
+ semanticShift: 'lethal_or_destructive',
+ explanation: 'The Latin-derived -cide ending tends to foreground killing or eradication.',
+ rewriteTemplates: [
+ '{domain} management',
+ '{domain} mitigation',
+ '{domain} control',
+ '{domain} stewardship'
+ ],
+ fallbackTemplates: [
+ 'mitigation',
+ 'management',
+ 'deterrence',
+ 'stewardship'
+ ]
+ },
+ {
+ id: 'latin-destructive-adjective',
+ family: 'destructive_suffix',
+ kind: 'suffix',
+ partOfSpeech: 'adjective',
+ affixes: ['cidal'],
+ patterns: [/\b([a-z]+?)cidal(ly)?\b/gi, /\bcidal(ly)?\b/gi],
+ severity: 'medium',
+ confidence: 0.84,
+ semanticShift: 'destructive_operational_framing',
+ explanation: 'The Latin-derived -cidal ending can make otherwise neutral activity sound destructive.',
+ rewriteTemplates: [
+ 'focused on {domain} management',
+ 'used for {domain} mitigation',
+ 'intended for {domain} control'
+ ],
+ fallbackTemplates: [
+ 'framed around mitigation',
+ 'framed around management',
+ 'operationally neutral'
+ ]
+ }
+ ];
+
+ const api = {
+ DOMAIN_ALIASES,
+ POLICIES
+ };
+
+ if (typeof module !== 'undefined' && module.exports) {
+ module.exports = api;
+ } else {
+ root.LATIN_AFFIX_POLICIES = POLICIES;
+ root.LATIN_DOMAIN_ALIASES = DOMAIN_ALIASES;
+ }
+})(typeof window !== 'undefined' ? window : globalThis);
diff --git a/js/tools/AntiClassifierTool.js b/js/tools/AntiClassifierTool.js
index 7dcc4d8..9b53d63 100644
--- a/js/tools/AntiClassifierTool.js
+++ b/js/tools/AntiClassifierTool.js
@@ -24,6 +24,7 @@ class AntiClassifierTool extends Tool {
acInput: '',
acOutput: '',
acError: '',
+ acLexemeAnalysis: { totalFindings: 0, findings: [], summary: 'No Latin-root wording findings.' },
acLoading: false,
acModel: localStorage.getItem('ac-model') || 'anthropic/claude-sonnet-4.6',
acModels: models,
@@ -49,6 +50,13 @@ class AntiClassifierTool extends Tool {
? window.ANTICLASSIFIER_SYSTEM_PROMPT
: '';
},
+ acRefreshLexemeAnalysis: function() {
+ if (typeof window === 'undefined' || !window.LexemeAnalysis || typeof window.LexemeAnalysis.analyze !== 'function') {
+ this.acLexemeAnalysis = { totalFindings: 0, findings: [], summary: 'Lexeme analysis unavailable.' };
+ return;
+ }
+ this.acLexemeAnalysis = window.LexemeAnalysis.analyze(this.acInput);
+ },
acRun: async function() {
const apiKey = this.acGetApiKey();
if (!apiKey) {
@@ -123,6 +131,39 @@ class AntiClassifierTool extends Tool {
if (this.acOutput) {
this.copyToClipboard(this.acOutput);
}
+ },
+ acGetLexemeAnalysis: function() {
+ return this.acLexemeAnalysis || { totalFindings: 0, findings: [], summary: 'No Latin-root wording findings.' };
+ },
+ acNeutralizeInput: function() {
+ const analysis = this.acGetLexemeAnalysis();
+ if (!analysis.totalFindings || !window.LexemeAnalysis || typeof window.LexemeAnalysis.neutralizeText !== 'function') {
+ return;
+ }
+ this.acInput = window.LexemeAnalysis.neutralizeText(this.acInput, analysis);
+ if (typeof this.showNotification === 'function') {
+ this.showNotification('Applied neutral Latin-root rewrites', 'success', 'fas fa-seedling');
+ }
+ },
+ acApplyLexemeRewrite: function(term, rewrite) {
+ if (!term || !rewrite) {
+ return;
+ }
+ const escapedTerm = String(term).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ this.acInput = this.acInput.replace(new RegExp('\\b' + escapedTerm + '\\b', 'i'), rewrite);
+ if (typeof this.showNotification === 'function') {
+ this.showNotification('Applied rewrite for ' + term, 'success', 'fas fa-pen');
+ }
+ }
+ };
+ }
+
+ getVueWatchers() {
+ return {
+ acInput: function() {
+ if (typeof this.acRefreshLexemeAnalysis === 'function') {
+ this.acRefreshLexemeAnalysis();
+ }
}
};
}
diff --git a/js/tools/PromptCraftTool.js b/js/tools/PromptCraftTool.js
index 0a8e0af..49f21c8 100644
--- a/js/tools/PromptCraftTool.js
+++ b/js/tools/PromptCraftTool.js
@@ -21,6 +21,7 @@ class PromptCraftTool extends Tool {
pcInput: '',
pcOutput: '',
pcOutputs: [],
+ pcLexemeAnalysis: { totalFindings: 0, findings: [], summary: 'No Latin-root wording findings.' },
pcStrategy: 'rephrase',
pcModel: localStorage.getItem('pc-model') || 'nousresearch/hermes-3-llama-3.1-405b',
pcTemperature,
@@ -78,6 +79,13 @@ class PromptCraftTool extends Tool {
}
return base;
},
+ pcRefreshLexemeAnalysis: function() {
+ if (typeof window === 'undefined' || !window.LexemeAnalysis || typeof window.LexemeAnalysis.analyze !== 'function') {
+ this.pcLexemeAnalysis = { totalFindings: 0, findings: [], summary: 'Lexeme analysis unavailable.' };
+ return;
+ }
+ this.pcLexemeAnalysis = window.LexemeAnalysis.analyze(this.pcInput);
+ },
pcRunMutation: async function() {
const apiKey = this.pcGetApiKey();
if (!apiKey) {
@@ -163,6 +171,39 @@ class PromptCraftTool extends Tool {
},
pcUseAsInput: function(text) {
this.pcInput = text;
+ },
+ pcGetLexemeAnalysis: function() {
+ return this.pcLexemeAnalysis || { totalFindings: 0, findings: [], summary: 'No Latin-root wording findings.' };
+ },
+ pcNeutralizeInput: function() {
+ const analysis = this.pcGetLexemeAnalysis();
+ if (!analysis.totalFindings || !window.LexemeAnalysis || typeof window.LexemeAnalysis.neutralizeText !== 'function') {
+ return;
+ }
+ this.pcInput = window.LexemeAnalysis.neutralizeText(this.pcInput, analysis);
+ if (typeof this.showNotification === 'function') {
+ this.showNotification('Applied neutral Latin-root rewrites', 'success', 'fas fa-seedling');
+ }
+ },
+ pcApplyLexemeRewrite: function(term, rewrite) {
+ if (!term || !rewrite) {
+ return;
+ }
+ const escapedTerm = String(term).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ this.pcInput = this.pcInput.replace(new RegExp('\\b' + escapedTerm + '\\b', 'i'), rewrite);
+ if (typeof this.showNotification === 'function') {
+ this.showNotification('Applied rewrite for ' + term, 'success', 'fas fa-pen');
+ }
+ }
+ };
+ }
+
+ getVueWatchers() {
+ return {
+ pcInput: function() {
+ if (typeof this.pcRefreshLexemeAnalysis === 'function') {
+ this.pcRefreshLexemeAnalysis();
+ }
}
};
}
diff --git a/package.json b/package.json
index 64a1606..117727e 100644
--- a/package.json
+++ b/package.json
@@ -13,9 +13,11 @@
"start": "serve dist -l 8080",
"preview": "npm run build && serve dist -l 8080",
"test": "node tests/test_universal.js",
+ "test:lexeme": "node tests/test_lexeme_analysis.js",
+ "test:lexeme-ui": "node tests/test_lexeme_ui_surface.js",
"test:universal": "node tests/test_universal.js",
"test:steg": "node tests/test_steganography_options.js",
- "test:all": "npm run test:universal && npm run test:steg",
+ "test:all": "npm run test:universal && npm run test:steg && npm run test:lexeme && npm run test:lexeme-ui",
"precommit": "npm run test:all"
},
"repository": {
diff --git a/templates/anticlassifier.html b/templates/anticlassifier.html
index c238120..1a3a871 100644
--- a/templates/anticlassifier.html
+++ b/templates/anticlassifier.html
@@ -13,6 +13,48 @@
+
+
+
+
+
+
+
+ {{ finding.term }}
+ {{ finding.partOfSpeech }}
+ {{ finding.affixes.join(', ') }}
+ confidence {{ Math.round(finding.confidence * 100) }}%
+
+
+ Domain: {{ finding.semanticDomain }}
+ Domain: unresolved root
+
+
+
{{ finding.rationale }}
+
+
+ {{ rewrite }}
+
+
+
+
+
+
Model
diff --git a/templates/promptcraft.html b/templates/promptcraft.html
index 98a80ef..f8891f7 100644
--- a/templates/promptcraft.html
+++ b/templates/promptcraft.html
@@ -12,6 +12,48 @@
+
+
+
+
+
+
+
+ {{ finding.term }}
+ {{ finding.partOfSpeech }}
+ {{ finding.affixes.join(', ') }}
+ confidence {{ Math.round(finding.confidence * 100) }}%
+
+
+ Domain: {{ finding.semanticDomain }}
+ Domain: unresolved root
+
+
+
{{ finding.rationale }}
+
+
+ {{ rewrite }}
+
+
+
+
+
+
Strategy
diff --git a/tests/test_lexeme_analysis.js b/tests/test_lexeme_analysis.js
new file mode 100644
index 0000000..1c739d3
--- /dev/null
+++ b/tests/test_lexeme_analysis.js
@@ -0,0 +1,40 @@
+#!/usr/bin/env node
+
+const assert = require('assert');
+const path = require('path');
+
+const lexemeAnalysis = require(path.join(__dirname, '..', 'js', 'core', 'lexemeAnalysis.js'));
+
+const empty = lexemeAnalysis.analyze('');
+assert.strictEqual(empty.totalFindings, 0, 'Empty input should yield no findings');
+
+const nounCase = lexemeAnalysis.analyze('Need benign pesticide framing.');
+assert.strictEqual(nounCase.totalFindings, 1, 'Known -cide noun should be detected');
+assert.strictEqual(nounCase.findings[0].family, 'destructive_suffix');
+assert.strictEqual(nounCase.findings[0].semanticDomain, 'pest');
+assert.strictEqual(nounCase.findings[0].primaryRewrite, 'pest management');
+
+const adjectiveCase = lexemeAnalysis.analyze('Looking for non-cidal wording in this prompt.');
+assert.strictEqual(adjectiveCase.totalFindings, 1, 'Standalone -cidal wording should be detected');
+assert.strictEqual(adjectiveCase.findings[0].partOfSpeech, 'adjective');
+assert.ok(
+ adjectiveCase.findings[0].primaryRewrite.includes('mitigation'),
+ 'Standalone adjectival form should fall back to mitigation-oriented rewrite'
+);
+
+const aliasCase = lexemeAnalysis.analyze('A bactericidal workflow should sound less destructive.');
+assert.strictEqual(aliasCase.totalFindings, 1, 'Alias-root adjectival case should be detected');
+assert.strictEqual(aliasCase.findings[0].semanticDomain, 'bacterial');
+assert.ok(
+ aliasCase.findings[0].rewrites.some((rewrite) => rewrite.includes('bacterial management')),
+ 'Alias roots should resolve into domain-specific rewrites'
+);
+
+const neutralized = lexemeAnalysis.neutralizeText(
+ 'Pesticide and bactericidal wording should be softened.',
+ lexemeAnalysis.analyze('Pesticide and bactericidal wording should be softened.')
+);
+assert.ok(neutralized.includes('Pest Management'), 'Neutralization should preserve leading capitalization');
+assert.ok(neutralized.includes('bacterial management'), 'Neutralization should replace adjectival form');
+
+console.log('Lexeme analysis tests passed');
diff --git a/tests/test_lexeme_ui_surface.js b/tests/test_lexeme_ui_surface.js
new file mode 100644
index 0000000..cf3e2a8
--- /dev/null
+++ b/tests/test_lexeme_ui_surface.js
@@ -0,0 +1,23 @@
+#!/usr/bin/env node
+
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+
+const projectRoot = path.join(__dirname, '..');
+const promptcraftTemplate = fs.readFileSync(path.join(projectRoot, 'templates', 'promptcraft.html'), 'utf8');
+const anticlassifierTemplate = fs.readFileSync(path.join(projectRoot, 'templates', 'anticlassifier.html'), 'utf8');
+const promptcraftTool = fs.readFileSync(path.join(projectRoot, 'js', 'tools', 'PromptCraftTool.js'), 'utf8');
+const anticlassifierTool = fs.readFileSync(path.join(projectRoot, 'js', 'tools', 'AntiClassifierTool.js'), 'utf8');
+const indexTemplate = fs.readFileSync(path.join(projectRoot, 'index.template.html'), 'utf8');
+
+assert.ok(promptcraftTemplate.includes('Latin-Root Analysis'), 'PromptCraft should expose the analysis card');
+assert.ok(promptcraftTemplate.includes('pcNeutralizeInput'), 'PromptCraft should expose neutralization action');
+assert.ok(anticlassifierTemplate.includes('Latin-Root Analysis'), 'Anti-Classifier should expose the analysis card');
+assert.ok(anticlassifierTemplate.includes('acNeutralizeInput'), 'Anti-Classifier should expose neutralization action');
+assert.ok(promptcraftTool.includes('pcGetLexemeAnalysis'), 'PromptCraft tool should bind shared analysis');
+assert.ok(anticlassifierTool.includes('acGetLexemeAnalysis'), 'Anti-Classifier tool should bind shared analysis');
+assert.ok(indexTemplate.includes('js/data/latinAffixPolicies.js'), 'Index template should load affix policy data');
+assert.ok(indexTemplate.includes('js/core/lexemeAnalysis.js'), 'Index template should load shared lexeme analysis engine');
+
+console.log('Lexeme UI surface tests passed');
From 789b28b5ca318fff8e31a3cb54e4a3bbf394527b Mon Sep 17 00:00:00 2001
From: GangGreenTemperTatum
<104169244+GangGreenTemperTatum@users.noreply.github.com>
Date: Sun, 5 Apr 2026 15:33:42 -0400
Subject: [PATCH 2/2] Expand Latin-root analysis across prompt inputs
---
js/data/latinAffixPolicies.js | 65 +++++++++++++++++++++++++++++++++
js/tools/BijectionTool.js | 41 +++++++++++++++++++++
js/tools/MutationTool.js | 42 ++++++++++++++++++++-
js/tools/TransformTool.js | 35 +++++++++++++++++-
templates/bijection.html | 35 ++++++++++++++++++
templates/fuzzer.html | 36 +++++++++++++++++-
templates/transforms.html | 37 ++++++++++++++++++-
tests/test_lexeme_analysis.js | 30 +++++++++++++++
tests/test_lexeme_ui_surface.js | 15 ++++++++
9 files changed, 332 insertions(+), 4 deletions(-)
diff --git a/js/data/latinAffixPolicies.js b/js/data/latinAffixPolicies.js
index fed4f51..7e2a955 100644
--- a/js/data/latinAffixPolicies.js
+++ b/js/data/latinAffixPolicies.js
@@ -70,6 +70,71 @@
'framed around management',
'operationally neutral'
]
+ },
+ {
+ id: 'latin-eradication-verbs',
+ family: 'eradication_language',
+ kind: 'surface_pattern',
+ partOfSpeech: 'verb',
+ affixes: ['eradicate', 'exterminate', 'annihilate', 'obliterate'],
+ patterns: [
+ /\b(?:eradicate|eradicates|eradicated|eradicating|eradication)\b/gi,
+ /\b(?:exterminate|exterminates|exterminated|exterminating|extermination)\b/gi,
+ /\b(?:annihilate|annihilates|annihilated|annihilating|annihilation)\b/gi,
+ /\b(?:obliterate|obliterates|obliterated|obliterating|obliteration)\b/gi
+ ],
+ severity: 'high',
+ confidence: 0.88,
+ semanticShift: 'total_removal',
+ explanation: 'Eradication language implies total destruction rather than bounded handling.',
+ fallbackTemplates: [
+ 'reduce',
+ 'manage',
+ 'contain',
+ 'address'
+ ]
+ },
+ {
+ id: 'latin-suppression-language',
+ family: 'suppression_language',
+ kind: 'surface_pattern',
+ partOfSpeech: 'verb',
+ affixes: ['neutralize', 'incapacitate', 'suppress'],
+ patterns: [
+ /\b(?:neutralize|neutralizes|neutralized|neutralizing|neutralization)\b/gi,
+ /\b(?:incapacitate|incapacitates|incapacitated|incapacitating|incapacitation)\b/gi,
+ /\b(?:suppress|suppresses|suppressed|suppressing|suppression)\b/gi
+ ],
+ severity: 'medium',
+ confidence: 0.82,
+ semanticShift: 'forceful_control',
+ explanation: 'Suppression language can read as coercive or force-centric rather than operationally neutral.',
+ fallbackTemplates: [
+ 'limit',
+ 'reduce',
+ 'constrain',
+ 'manage'
+ ]
+ },
+ {
+ id: 'latin-terminality-language',
+ family: 'terminality_language',
+ kind: 'surface_pattern',
+ partOfSpeech: 'adjective',
+ affixes: ['lethal', 'fatal', 'terminal', 'mortal'],
+ patterns: [
+ /\b(?:lethal|fatal|terminal|mortal)\b/gi
+ ],
+ severity: 'medium',
+ confidence: 0.8,
+ semanticShift: 'fatal_outcome_framing',
+ explanation: 'Terminality language frames outcomes as fatal or irreversible even when a softer operational phrasing would do.',
+ fallbackTemplates: [
+ 'high-risk',
+ 'severe',
+ 'critical',
+ 'unsafe'
+ ]
}
];
diff --git a/js/tools/BijectionTool.js b/js/tools/BijectionTool.js
index 8bf9f30..bc1606c 100644
--- a/js/tools/BijectionTool.js
+++ b/js/tools/BijectionTool.js
@@ -20,6 +20,7 @@ class BijectionTool extends Tool {
bijectionIncludeExamples: true,
bijectionAutoCopy: false,
bijectionInput: '',
+ bijectionLexemeAnalysis: { totalFindings: 0, findings: [], summary: 'No Latin-root wording findings.' },
bijectionMapping: {},
bijectionOutputs: []
};
@@ -27,6 +28,36 @@ class BijectionTool extends Tool {
getVueMethods() {
return {
+ bijectionRefreshLexemeAnalysis() {
+ if (typeof window === 'undefined' || !window.LexemeAnalysis || typeof window.LexemeAnalysis.analyze !== 'function') {
+ this.bijectionLexemeAnalysis = { totalFindings: 0, findings: [], summary: 'Lexeme analysis unavailable.' };
+ return;
+ }
+ this.bijectionLexemeAnalysis = window.LexemeAnalysis.analyze(this.bijectionInput);
+ },
+ bijectionGetLexemeAnalysis() {
+ return this.bijectionLexemeAnalysis || { totalFindings: 0, findings: [], summary: 'No Latin-root wording findings.' };
+ },
+ bijectionNeutralizeInput() {
+ const analysis = this.bijectionGetLexemeAnalysis();
+ if (!analysis.totalFindings || !window.LexemeAnalysis || typeof window.LexemeAnalysis.neutralizeText !== 'function') {
+ return;
+ }
+ this.bijectionInput = window.LexemeAnalysis.neutralizeText(this.bijectionInput, analysis);
+ if (typeof this.showNotification === 'function') {
+ this.showNotification('Applied neutral Latin-root rewrites', 'success', 'fas fa-seedling');
+ }
+ },
+ bijectionApplyLexemeRewrite(term, rewrite) {
+ if (!term || !rewrite) {
+ return;
+ }
+ const escapedTerm = String(term).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ this.bijectionInput = this.bijectionInput.replace(new RegExp('\\b' + escapedTerm + '\\b', 'i'), rewrite);
+ if (typeof this.showNotification === 'function') {
+ this.showNotification('Applied rewrite for ' + term, 'success', 'fas fa-pen');
+ }
+ },
generateBijectionMapping() {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.,!?';
const mapping = {};
@@ -229,6 +260,16 @@ Now, please translate and respond to this message in alphapr: ${encoded}`;
}
};
}
+
+ getVueWatchers() {
+ return {
+ bijectionInput: function() {
+ if (typeof this.bijectionRefreshLexemeAnalysis === 'function') {
+ this.bijectionRefreshLexemeAnalysis();
+ }
+ }
+ };
+ }
}
if (typeof module !== 'undefined' && module.exports) {
diff --git a/js/tools/MutationTool.js b/js/tools/MutationTool.js
index 85a30b0..aac960d 100644
--- a/js/tools/MutationTool.js
+++ b/js/tools/MutationTool.js
@@ -15,6 +15,7 @@ class MutationTool extends Tool {
getVueData() {
return {
fuzzerInput: '',
+ fuzzerLexemeAnalysis: { totalFindings: 0, findings: [], summary: 'No Latin-root wording findings.' },
fuzzerCount: 20,
fuzzerSeed: '',
fuzzUseRandomMix: true,
@@ -30,6 +31,36 @@ class MutationTool extends Tool {
getVueMethods() {
return {
+ fuzzerRefreshLexemeAnalysis: function() {
+ if (typeof window === 'undefined' || !window.LexemeAnalysis || typeof window.LexemeAnalysis.analyze !== 'function') {
+ this.fuzzerLexemeAnalysis = { totalFindings: 0, findings: [], summary: 'Lexeme analysis unavailable.' };
+ return;
+ }
+ this.fuzzerLexemeAnalysis = window.LexemeAnalysis.analyze(this.fuzzerInput);
+ },
+ fuzzerGetLexemeAnalysis: function() {
+ return this.fuzzerLexemeAnalysis || { totalFindings: 0, findings: [], summary: 'No Latin-root wording findings.' };
+ },
+ fuzzerNeutralizeInput: function() {
+ const analysis = this.fuzzerGetLexemeAnalysis();
+ if (!analysis.totalFindings || !window.LexemeAnalysis || typeof window.LexemeAnalysis.neutralizeText !== 'function') {
+ return;
+ }
+ this.fuzzerInput = window.LexemeAnalysis.neutralizeText(this.fuzzerInput, analysis);
+ if (typeof this.showNotification === 'function') {
+ this.showNotification('Applied neutral Latin-root rewrites', 'success', 'fas fa-seedling');
+ }
+ },
+ fuzzerApplyLexemeRewrite: function(term, rewrite) {
+ if (!term || !rewrite) {
+ return;
+ }
+ const escapedTerm = String(term).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ this.fuzzerInput = this.fuzzerInput.replace(new RegExp('\\b' + escapedTerm + '\\b', 'i'), rewrite);
+ if (typeof this.showNotification === 'function') {
+ this.showNotification('Applied rewrite for ' + term, 'success', 'fas fa-pen');
+ }
+ },
seededRandomFactory: function(seedStr) {
if (!seedStr) return Math.random;
let h = 1779033703 ^ seedStr.length;
@@ -106,6 +137,16 @@ class MutationTool extends Tool {
}
};
}
+
+ getVueWatchers() {
+ return {
+ fuzzerInput: function() {
+ if (typeof this.fuzzerRefreshLexemeAnalysis === 'function') {
+ this.fuzzerRefreshLexemeAnalysis();
+ }
+ }
+ };
+ }
}
// Export
@@ -116,4 +157,3 @@ if (typeof module !== 'undefined' && module.exports) {
}
-
diff --git a/js/tools/TransformTool.js b/js/tools/TransformTool.js
index 39b2b89..990ff1f 100644
--- a/js/tools/TransformTool.js
+++ b/js/tools/TransformTool.js
@@ -61,6 +61,7 @@ class TransformTool extends Tool {
return {
transformInput: 'Hello World',
+ transformLexemeAnalysis: { totalFindings: 0, findings: [], summary: 'No Latin-root wording findings.' },
transformOutput: '',
activeTransform: null,
transforms: transforms,
@@ -250,6 +251,36 @@ class TransformTool extends Tool {
}
return 'text';
},
+ transformRefreshLexemeAnalysis: function() {
+ if (typeof window === 'undefined' || !window.LexemeAnalysis || typeof window.LexemeAnalysis.analyze !== 'function') {
+ this.transformLexemeAnalysis = { totalFindings: 0, findings: [], summary: 'Lexeme analysis unavailable.' };
+ return;
+ }
+ this.transformLexemeAnalysis = window.LexemeAnalysis.analyze(this.transformInput);
+ },
+ transformGetLexemeAnalysis: function() {
+ return this.transformLexemeAnalysis || { totalFindings: 0, findings: [], summary: 'No Latin-root wording findings.' };
+ },
+ transformNeutralizeInput: function() {
+ const analysis = this.transformGetLexemeAnalysis();
+ if (!analysis.totalFindings || !window.LexemeAnalysis || typeof window.LexemeAnalysis.neutralizeText !== 'function') {
+ return;
+ }
+ this.transformInput = window.LexemeAnalysis.neutralizeText(this.transformInput, analysis);
+ if (typeof this.showNotification === 'function') {
+ this.showNotification('Applied neutral Latin-root rewrites', 'success', 'fas fa-seedling');
+ }
+ },
+ transformApplyLexemeRewrite: function(term, rewrite) {
+ if (!term || !rewrite) {
+ return;
+ }
+ const escapedTerm = String(term).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ this.transformInput = this.transformInput.replace(new RegExp('\\b' + escapedTerm + '\\b', 'i'), rewrite);
+ if (typeof this.showNotification === 'function') {
+ this.showNotification('Applied rewrite for ' + term, 'success', 'fas fa-pen');
+ }
+ },
openTransformOptions: function(transform, event) {
if (event) {
event.preventDefault();
@@ -623,6 +654,9 @@ class TransformTool extends Tool {
getVueWatchers() {
return {
transformInput() {
+ if (typeof this.transformRefreshLexemeAnalysis === 'function') {
+ this.transformRefreshLexemeAnalysis();
+ }
if (this.activeTransform && this.activeTab === 'transforms') {
const opts = this.getMergedOptionsForTransform(this.activeTransform.name);
this.transformOutput = this.activeTransform.func(this.transformInput, opts);
@@ -670,4 +704,3 @@ if (typeof module !== 'undefined' && module.exports) {
}
-
diff --git a/templates/bijection.html b/templates/bijection.html
index d5ba9ef..2336c5c 100644
--- a/templates/bijection.html
+++ b/templates/bijection.html
@@ -16,6 +16,41 @@
+
+
+
+
+
+
+ {{ finding.term }}
+ {{ finding.partOfSpeech }}
+ {{ finding.family }}
+ confidence {{ Math.round(finding.confidence * 100) }}%
+
+
+ Domain: {{ finding.semanticDomain }}
+ Domain: unresolved root
+
+
+
{{ finding.rationale }}
+
+
+ {{ rewrite }}
+
+
+
+
+
+
Bijection type
diff --git a/templates/fuzzer.html b/templates/fuzzer.html
index ffbc63e..62314b3 100644
--- a/templates/fuzzer.html
+++ b/templates/fuzzer.html
@@ -10,6 +10,40 @@
+
+
+
+
+
+
+ {{ finding.term }}
+ {{ finding.partOfSpeech }}
+ {{ finding.family }}
+ confidence {{ Math.round(finding.confidence * 100) }}%
+
+
+ Domain: {{ finding.semanticDomain }}
+ Domain: unresolved root
+
+
+
{{ finding.rationale }}
+
+
+ {{ rewrite }}
+
+
+
+
+
Cases
@@ -45,4 +79,4 @@
-
\ No newline at end of file
+
diff --git a/templates/transforms.html b/templates/transforms.html
index 4dd7795..dd76e92 100644
--- a/templates/transforms.html
+++ b/templates/transforms.html
@@ -21,6 +21,41 @@
/>
+
+
-
\ No newline at end of file
+
diff --git a/tests/test_lexeme_analysis.js b/tests/test_lexeme_analysis.js
index 1c739d3..a30d87a 100644
--- a/tests/test_lexeme_analysis.js
+++ b/tests/test_lexeme_analysis.js
@@ -30,6 +30,22 @@ assert.ok(
'Alias roots should resolve into domain-specific rewrites'
);
+const eradicationCase = lexemeAnalysis.analyze('We should eradicate this issue before release.');
+assert.strictEqual(eradicationCase.totalFindings, 1, 'Eradication language should be detected');
+assert.strictEqual(eradicationCase.findings[0].family, 'eradication_language');
+assert.ok(
+ eradicationCase.findings[0].rewrites.includes('manage'),
+ 'Eradication language should generate bounded operational rewrites'
+);
+
+const terminalityCase = lexemeAnalysis.analyze('This creates a lethal failure mode.');
+assert.strictEqual(terminalityCase.totalFindings, 1, 'Terminality language should be detected');
+assert.strictEqual(terminalityCase.findings[0].family, 'terminality_language');
+assert.ok(
+ terminalityCase.findings[0].rewrites.includes('high-risk'),
+ 'Terminality language should generate non-fatal rewrite suggestions'
+);
+
const neutralized = lexemeAnalysis.neutralizeText(
'Pesticide and bactericidal wording should be softened.',
lexemeAnalysis.analyze('Pesticide and bactericidal wording should be softened.')
@@ -37,4 +53,18 @@ const neutralized = lexemeAnalysis.neutralizeText(
assert.ok(neutralized.includes('Pest Management'), 'Neutralization should preserve leading capitalization');
assert.ok(neutralized.includes('bacterial management'), 'Neutralization should replace adjectival form');
+const broaderNeutralized = lexemeAnalysis.neutralizeText(
+ 'We should eradicate lethal language.',
+ lexemeAnalysis.analyze('We should eradicate lethal language.')
+);
+assert.ok(
+ broaderNeutralized.includes('reduce') ||
+ broaderNeutralized.includes('manage') ||
+ broaderNeutralized.includes('contain') ||
+ broaderNeutralized.includes('address'),
+ 'Broader policy families should neutralize loaded verbs'
+);
+assert.ok(!broaderNeutralized.includes('eradicate'), 'Loaded verb should be removed from neutralized output');
+assert.ok(broaderNeutralized.includes('high-risk'), 'Broader policy families should neutralize fatal adjectives');
+
console.log('Lexeme analysis tests passed');
diff --git a/tests/test_lexeme_ui_surface.js b/tests/test_lexeme_ui_surface.js
index cf3e2a8..d0d3171 100644
--- a/tests/test_lexeme_ui_surface.js
+++ b/tests/test_lexeme_ui_surface.js
@@ -7,16 +7,31 @@ const path = require('path');
const projectRoot = path.join(__dirname, '..');
const promptcraftTemplate = fs.readFileSync(path.join(projectRoot, 'templates', 'promptcraft.html'), 'utf8');
const anticlassifierTemplate = fs.readFileSync(path.join(projectRoot, 'templates', 'anticlassifier.html'), 'utf8');
+const bijectionTemplate = fs.readFileSync(path.join(projectRoot, 'templates', 'bijection.html'), 'utf8');
+const fuzzerTemplate = fs.readFileSync(path.join(projectRoot, 'templates', 'fuzzer.html'), 'utf8');
+const transformsTemplate = fs.readFileSync(path.join(projectRoot, 'templates', 'transforms.html'), 'utf8');
const promptcraftTool = fs.readFileSync(path.join(projectRoot, 'js', 'tools', 'PromptCraftTool.js'), 'utf8');
const anticlassifierTool = fs.readFileSync(path.join(projectRoot, 'js', 'tools', 'AntiClassifierTool.js'), 'utf8');
+const bijectionTool = fs.readFileSync(path.join(projectRoot, 'js', 'tools', 'BijectionTool.js'), 'utf8');
+const mutationTool = fs.readFileSync(path.join(projectRoot, 'js', 'tools', 'MutationTool.js'), 'utf8');
+const transformTool = fs.readFileSync(path.join(projectRoot, 'js', 'tools', 'TransformTool.js'), 'utf8');
const indexTemplate = fs.readFileSync(path.join(projectRoot, 'index.template.html'), 'utf8');
assert.ok(promptcraftTemplate.includes('Latin-Root Analysis'), 'PromptCraft should expose the analysis card');
assert.ok(promptcraftTemplate.includes('pcNeutralizeInput'), 'PromptCraft should expose neutralization action');
assert.ok(anticlassifierTemplate.includes('Latin-Root Analysis'), 'Anti-Classifier should expose the analysis card');
assert.ok(anticlassifierTemplate.includes('acNeutralizeInput'), 'Anti-Classifier should expose neutralization action');
+assert.ok(bijectionTemplate.includes('Latin-Root Analysis'), 'Bijection should expose the analysis card');
+assert.ok(bijectionTemplate.includes('bijectionNeutralizeInput'), 'Bijection should expose neutralization action');
+assert.ok(fuzzerTemplate.includes('Latin-Root Analysis'), 'Mutation Lab should expose the analysis card');
+assert.ok(fuzzerTemplate.includes('fuzzerNeutralizeInput'), 'Mutation Lab should expose neutralization action');
+assert.ok(transformsTemplate.includes('Latin-Root Analysis'), 'Transforms should expose the analysis card');
+assert.ok(transformsTemplate.includes('transformNeutralizeInput'), 'Transforms should expose neutralization action');
assert.ok(promptcraftTool.includes('pcGetLexemeAnalysis'), 'PromptCraft tool should bind shared analysis');
assert.ok(anticlassifierTool.includes('acGetLexemeAnalysis'), 'Anti-Classifier tool should bind shared analysis');
+assert.ok(bijectionTool.includes('bijectionGetLexemeAnalysis'), 'Bijection tool should bind shared analysis');
+assert.ok(mutationTool.includes('fuzzerGetLexemeAnalysis'), 'Mutation tool should bind shared analysis');
+assert.ok(transformTool.includes('transformGetLexemeAnalysis'), 'Transform tool should bind shared analysis');
assert.ok(indexTemplate.includes('js/data/latinAffixPolicies.js'), 'Index template should load affix policy data');
assert.ok(indexTemplate.includes('js/core/lexemeAnalysis.js'), 'Index template should load shared lexeme analysis engine');