Add Latin-root lexeme analysis to prompt tools

This commit is contained in:
GangGreenTemperTatum
2026-04-05 15:23:09 -04:00
parent d0918fe5ea
commit 0a27bc73fd
11 changed files with 638 additions and 2 deletions
+137 -1
View File
@@ -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);
}
}
+2
View File
@@ -410,6 +410,7 @@
<script src="js/data/endSequences.js"></script>
<script src="js/data/openrouterModels.js"></script>
<script src="js/data/anticlassifierPrompt.js"></script>
<script src="js/data/latinAffixPolicies.js"></script>
<!-- Load Configuration and Utilities (before modules that depend on them) -->
<script src="js/config/constants.js"></script>
@@ -426,6 +427,7 @@
<script src="js/core/steganography.js"></script>
<script src="js/core/transformOptions.js"></script>
<script src="js/core/decoder.js"></script>
<script src="js/core/lexemeAnalysis.js"></script>
<!-- Load Tool System -->
<script src="js/tools/Tool.js"></script>
+180
View File
@@ -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);
+87
View File
@@ -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);
+41
View File
@@ -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();
}
}
};
}
+41
View File
@@ -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();
}
}
};
}
+3 -1
View File
@@ -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": {
+42
View File
@@ -13,6 +13,48 @@
</label>
</div>
<div v-if="acGetLexemeAnalysis().totalFindings" class="lexeme-analysis-card">
<div class="lexeme-analysis-header">
<div>
<div class="lexeme-analysis-kicker">Latin-Root Analysis</div>
<h4>{{ acGetLexemeAnalysis().summary }}</h4>
<p>This input contains affix patterns that skew toward destructive or lethal framing. Use the generated rewrites to neutralize tone before transformation.</p>
</div>
<button type="button" class="action-button copy lexeme-neutralize-btn" @click="acNeutralizeInput">
<i class="fas fa-seedling"></i> Neutralize flagged terms
</button>
</div>
<div class="lexeme-analysis-list">
<div v-for="finding in acGetLexemeAnalysis().findings" :key="finding.id" class="lexeme-analysis-item">
<div class="lexeme-analysis-item-top">
<div class="lexeme-analysis-term-group">
<code class="lexeme-term">{{ finding.term }}</code>
<span class="lexeme-chip">{{ finding.partOfSpeech }}</span>
<span class="lexeme-chip">{{ finding.affixes.join(', ') }}</span>
<span class="lexeme-chip">confidence {{ Math.round(finding.confidence * 100) }}%</span>
</div>
<div class="lexeme-analysis-domain">
<span v-if="finding.semanticDomain"><strong>Domain:</strong> {{ finding.semanticDomain }}</span>
<span v-else><strong>Domain:</strong> unresolved root</span>
</div>
</div>
<p class="lexeme-analysis-rationale">{{ finding.rationale }}</p>
<div class="lexeme-analysis-rewrites">
<button
v-for="rewrite in finding.rewrites"
:key="finding.id + '-' + rewrite"
type="button"
class="lexeme-rewrite-btn"
@click="acApplyLexemeRewrite(finding.term, rewrite)"
>
{{ rewrite }}
</button>
</div>
</div>
</div>
</div>
<div class="options-grid ac-options">
<label>
Model
+42
View File
@@ -12,6 +12,48 @@
</label>
</div>
<div v-if="pcGetLexemeAnalysis().totalFindings" class="lexeme-analysis-card">
<div class="lexeme-analysis-header">
<div>
<div class="lexeme-analysis-kicker">Latin-Root Analysis</div>
<h4>{{ pcGetLexemeAnalysis().summary }}</h4>
<p>This input contains affix patterns that skew toward destructive or lethal framing. Use the generated rewrites to neutralize tone before mutation.</p>
</div>
<button type="button" class="action-button copy lexeme-neutralize-btn" @click="pcNeutralizeInput">
<i class="fas fa-seedling"></i> Neutralize flagged terms
</button>
</div>
<div class="lexeme-analysis-list">
<div v-for="finding in pcGetLexemeAnalysis().findings" :key="finding.id" class="lexeme-analysis-item">
<div class="lexeme-analysis-item-top">
<div class="lexeme-analysis-term-group">
<code class="lexeme-term">{{ finding.term }}</code>
<span class="lexeme-chip">{{ finding.partOfSpeech }}</span>
<span class="lexeme-chip">{{ finding.affixes.join(', ') }}</span>
<span class="lexeme-chip">confidence {{ Math.round(finding.confidence * 100) }}%</span>
</div>
<div class="lexeme-analysis-domain">
<span v-if="finding.semanticDomain"><strong>Domain:</strong> {{ finding.semanticDomain }}</span>
<span v-else><strong>Domain:</strong> unresolved root</span>
</div>
</div>
<p class="lexeme-analysis-rationale">{{ finding.rationale }}</p>
<div class="lexeme-analysis-rewrites">
<button
v-for="rewrite in finding.rewrites"
:key="finding.id + '-' + rewrite"
type="button"
class="lexeme-rewrite-btn"
@click="pcApplyLexemeRewrite(finding.term, rewrite)"
>
{{ rewrite }}
</button>
</div>
</div>
</div>
</div>
<div class="pc-controls">
<div class="pc-strategies">
<div class="pc-label" id="pc-strategy-label">Strategy</div>
+40
View File
@@ -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');
+23
View File
@@ -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');