Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions policy/component-nouns.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Project-registerable component nouns (issue #10 direction #3).
# Each noun behaves like the built-in 'API' noun: when it appears with a
# live/recorded-state qualifier (alive, up, down, responding, running, live,
# health, status, right now, today, still, currently, any more), the prompt
# routes to the runtime family. Precision > recall: a noun alone never routes.
# Per-project: drop a copy next to tells.yaml in your own checkout to register
# your domain nouns.
nouns:
- fleet-hub
- connector
- ingest
- vendor-archive
- store
42 changes: 42 additions & 0 deletions src/matcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,50 @@ function loadPolicy(filePath) {
if (!parsed || !Array.isArray(parsed.tells) || !Array.isArray(parsed.negatives)) {
throw new Error(`invalid tells policy: ${filePath}`);
}
// Project-registerable component nouns (issue #10 direction #3): an optional
// companion file listing domain nouns that should behave like the built-in
// 'API' noun for runtime probing.
const nounsFile = `${filePath.replace(/tells\.yaml$/u, '')}component-nouns.yaml`;
let componentNouns = [];
try {
const raw = YAML.parse(fs.readFileSync(nounsFile, 'utf8'));
if (raw && Array.isArray(raw.nouns)) componentNouns = raw.nouns.map(String).filter(Boolean);
} catch { /* optional file */ }
parsed.componentNouns = componentNouns;
return parsed;
}

function escapeRegex(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

// Live/recorded-state qualifiers that, paired with a component noun, indicate a runtime probe.
const STATE_QUALIFIERS = ['alive', 'up', 'down', 'responding', 'responding now', 'running', 'live', 'health', 'status', 'right now', 'today', 'still', 'currently', 'any more'];

function matchComponentNoun(prompt, policy, clause, clauseIndex) {
const matches = [];
const nouns = policy.componentNouns || [];
if (nouns.length === 0) return matches;
const lower = clause.text.toLocaleLowerCase();
const escaped = nouns.map((n) => escapeRegex(n.toLocaleLowerCase()));
for (const noun of escaped) {
const idx = lower.indexOf(noun);
if (idx === -1) continue;
const hasQualifier = STATE_QUALIFIERS.some((q) => lower.includes(q));
if (!hasQualifier) continue;
matches.push({
id: 'runtime-component-noun',
family: 'runtime',
terminal: true,
phrase: noun,
text: clause.text.slice(idx, idx + noun.length),
span: { start: clause.start + idx, end: clause.start + idx + noun.length },
clause: { index: clauseIndex, text: clause.text, start: clause.start, end: clause.end },
});
}
return matches;
}

function compilePhrase(phrase) {
const pieces = String(phrase).split('*').map((piece) => escapeRegex(piece).replace(/\s+/g, '\\s+'));
const wildcard = '[\\p{L}\\p{N}_.$()/:@-]+(?:\\s+[\\p{L}\\p{N}_.$()/:@-]+){0,4}?';
Expand Down Expand Up @@ -104,6 +141,11 @@ function matchPrompt(prompt, policy) {
}
});
});
// Issue #10 direction #3: project-registerable component nouns behave like
// the built-in 'API' noun when paired with a live/recorded-state qualifier.
if ((policy.componentNouns || []).length > 0) {
matchComponentNoun(prompt, policy, clause, clauseIndex).forEach((m) => matches.push(m));
}
});

matches.sort((a, b) => a.span.start - b.span.start || a._order[0] - b._order[0] || a._order[1] - b._order[1]);
Expand Down
36 changes: 36 additions & 0 deletions test/component-nouns.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use strict';

// Regression tests for issue #10 direction #3: project-registerable component
// nouns behave like the built-in 'API' noun for runtime probing when paired
// with a live/recorded-state qualifier.

const test = require('node:test');
const assert = require('node:assert/strict');
const path = require('node:path');
const { loadPolicy, matchPrompt } = require('../src/matcher');

const policyPath = path.join(__dirname, '..', 'policy', 'tells.yaml');

test('custom component noun + state qualifier routes to runtime', () => {
const policy = loadPolicy(policyPath);
const f = matchPrompt('is the fleet-hub alive right now?', policy).map((m) => m.family);
assert.ok(f.includes('runtime'), `expected runtime in ${JSON.stringify(f)}`);
});

test('registered noun alone (no qualifier) does NOT route', () => {
const policy = loadPolicy(policyPath);
const f = matchPrompt('what is the fleet-hub', policy).map((m) => m.family);
assert.ok(!f.includes('runtime'), `expected no runtime in ${JSON.stringify(f)}`);
});

test('"store still read" routes to runtime via registered noun', () => {
const policy = loadPolicy(policyPath);
const f = matchPrompt('does the store still read sales?', policy).map((m) => m.family);
assert.ok(f.includes('runtime'), `expected runtime in ${JSON.stringify(f)}`);
});

test('absent glossary means no component-noun matches (backward compatible)', () => {
const policy = { tells: [], negatives: [], componentNouns: [] };
const f = matchPrompt('is the fleet-hub alive right now?', policy).map((m) => m.family);
assert.deepEqual(f, []);
});
Loading