@adia-ai/a2ui-retrieval 0.4.5 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/authoring/index.js +3 -7
- package/component-catalog.js +111 -0
- package/context-assembler.js +20 -14
- package/embedding/index.js +5 -2
- package/index.js +1 -1
- package/intent/clarity.js +4 -4
- package/intent/prompt-analyzer.js +16 -8
- package/package.json +1 -1
- package/authoring/pattern-promotion.js +0 -135
- package/authoring/synthetic-data.js +0 -446
- package/concept-mapper.js +0 -127
- package/embedding/embedding-retriever.js +0 -152
- package/pattern-library.js +0 -659
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,32 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
|
|
|
7
7
|
|
|
8
8
|
_No pending changes._
|
|
9
9
|
|
|
10
|
+
## [0.4.7] - 2026-05-12
|
|
11
|
+
|
|
12
|
+
### Removed — `embedding/embedding-retriever.js` + `concept-mapper.js` retired (§72, the §65 carry-over)
|
|
13
|
+
|
|
14
|
+
The pattern-embeddings retrieval surface — `embedding-retriever.js` and its sole consumer `concept-mapper.js` (dead since v0.4.6 §64 retired `pattern-library.js`) — has been deleted from `packages/a2ui/retrieval/`. Companion artifacts in `@adia-ai/a2ui-corpus` (`pattern-embeddings.json` data file + `./pattern-embeddings` subpath export) and repo-side (`scripts/build/embeddings.mjs` + `build:embeddings` / `build:embeddings:all` npm scripts) retired in the same arc.
|
|
15
|
+
|
|
16
|
+
`embedding/index.js` no longer re-exports from `embedding-retriever.js`. The remaining surface: `embedding-provider.js` (provider abstraction over Voyage / OpenAI) + `chunk-embedding-retriever.js` (the canonical embedding-based retriever, scoped to chunks).
|
|
17
|
+
|
|
18
|
+
### Changed — `intent/prompt-analyzer.js` `getVocab()` reads canonical catalog (§72)
|
|
19
|
+
|
|
20
|
+
Migrated from `@adia-ai/a2ui-corpus/patterns/_components.json` (retired) to `@adia-ai/a2ui-corpus/catalog-a2ui_0_9.json` (the canonical v0.9 catalog). Vocabulary now built from `components[name].x-adiaui.synonyms.tags`. Same `displayList` + `allowedSet` output shape; no behavior change for downstream callers.
|
|
21
|
+
|
|
22
|
+
## [0.4.6] - 2026-05-12
|
|
23
|
+
|
|
24
|
+
### Changed — `pattern-library.js` retired, consumers migrate to `composition-library` (§64 multi-step, 2026-05-12)
|
|
25
|
+
|
|
26
|
+
The legacy `pattern-library.js` retrieval surface is retired in v0.4.6. Compositions (`@adia-ai/a2ui-compose/strategies/zettel/composition-library.js`) are now the canonical retrieval source — the §41 unification of compositions + annotated chunks made `pattern-library.js` redundant. Six-step migration (commits `cd11939d` / `67d3075c` / `c2336895` / `7cd42ca4` / `9abcd8ce` / `a4814384`):
|
|
27
|
+
|
|
28
|
+
- **`pattern-library.js` retired** (§64 step 6). `reference.js` now delegates to `composition-library` for all reads; the file remains as a thin shim that re-exports the new path while emitting a one-shot deprecation warn for direct importers.
|
|
29
|
+
- **`component-catalog.js` carved out of `pattern-library`** (§64 step 1). Previously the catalog ride-along on the pattern surface; now lives at its own module path so consumers can drop `pattern-library` without losing the catalog. Re-exported from the package barrel.
|
|
30
|
+
- **`context-assembler.js` + `intent/clarity.js` migrated to `composition-library`** (§64 step 4). Both modules previously asked `pattern-library.search(intent)`; now ask `compositionLibrary.search(intent)`. Behavior preserved — the new lookup returns the same shape post-§41 normalization.
|
|
31
|
+
- **`authoring/index.js` + `authoring/pattern-promotion.js` + `authoring/synthetic-data.js` retired** (§64 step 2). All three were dead code by §63 (pattern-promotion fired on a surface that no consumer exercised; synthetic-data targeted the pattern shape). Files deleted; barrel re-exports cleaned.
|
|
32
|
+
- **`index.js`** — barrel updated to reflect the carved-out + retired modules.
|
|
33
|
+
|
|
34
|
+
See root [CHANGELOG.md `[Unreleased]`](../../../CHANGELOG.md) and `@adia-ai/a2ui-compose@[Unreleased]` for the cross-cutting arc narrative.
|
|
35
|
+
|
|
10
36
|
## [0.4.5] - 2026-05-12
|
|
11
37
|
|
|
12
38
|
### Ride-along (no source changes)
|
package/authoring/index.js
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @adia-ai/a2ui-retrieval/authoring — corpus authoring helpers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* the retrieval package because they share the catalog + pattern-library
|
|
8
|
-
* surfaces, but logically a separate concern from lookup.
|
|
4
|
+
* Web-research helpers used by `compose/core/generator.js` to enrich
|
|
5
|
+
* intent before generation. Synthetic-data + pattern-promotion retired
|
|
6
|
+
* in §64 alongside `pattern-library.js`.
|
|
9
7
|
*/
|
|
10
8
|
|
|
11
|
-
export * from './synthetic-data.js';
|
|
12
9
|
export * from './web-research.js';
|
|
13
|
-
export * from './pattern-promotion.js';
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component Catalog — Loads `.a2ui.json` sidecars from web-components/.
|
|
3
|
+
*
|
|
4
|
+
* The `.a2ui.json` sidecars are hand-authored component contracts that
|
|
5
|
+
* live alongside their components in `packages/web-components/components/`.
|
|
6
|
+
* They are part of the component library — NOT the training corpus — and
|
|
7
|
+
* are the canonical source of truth for component shape (v0.9 JSON Schema
|
|
8
|
+
* with `x-adiaui` extension for retrieval signals).
|
|
9
|
+
*
|
|
10
|
+
* §66 wired these into `buildSystemPrompt` as the authoritative component
|
|
11
|
+
* descriptor. §64 split this loader out of the now-retired
|
|
12
|
+
* `pattern-library.js` so it survives the patterns corpus deletion.
|
|
13
|
+
*
|
|
14
|
+
* Works in both Node.js (readdir/readFile) and the browser (Vite glob).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const IS_NODE =
|
|
18
|
+
typeof process !== 'undefined' &&
|
|
19
|
+
typeof process.versions?.node === 'string';
|
|
20
|
+
|
|
21
|
+
/** @type {Map<string, object>} */
|
|
22
|
+
const _componentData = new Map();
|
|
23
|
+
|
|
24
|
+
let _loaded = false;
|
|
25
|
+
let _loadPromise = null;
|
|
26
|
+
|
|
27
|
+
// Vite resolves this at build time; at runtime in Node the variable is unused.
|
|
28
|
+
let _globA2UIModules = null;
|
|
29
|
+
if (!IS_NODE) {
|
|
30
|
+
try {
|
|
31
|
+
_globA2UIModules = import.meta.glob('../../web-components/components/**/*.a2ui.json', {
|
|
32
|
+
query: '?raw',
|
|
33
|
+
import: 'default',
|
|
34
|
+
});
|
|
35
|
+
} catch {
|
|
36
|
+
// Not in a Vite context — no component data
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function _loadNode() {
|
|
41
|
+
const fs = await import(/* @vite-ignore */ 'node:fs/promises');
|
|
42
|
+
const path = await import(/* @vite-ignore */ 'node:path');
|
|
43
|
+
const url = await import(/* @vite-ignore */ 'node:url');
|
|
44
|
+
|
|
45
|
+
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
46
|
+
const componentsDir = path.resolve(__dirname, '..', '..', 'web-components', 'components');
|
|
47
|
+
|
|
48
|
+
let compEntries;
|
|
49
|
+
try {
|
|
50
|
+
compEntries = await fs.readdir(componentsDir, { withFileTypes: true });
|
|
51
|
+
} catch {
|
|
52
|
+
return; // components dir doesn't exist — skip
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const compEntry of compEntries) {
|
|
56
|
+
if (!compEntry.isDirectory()) continue;
|
|
57
|
+
const a2uiPath = path.join(componentsDir, compEntry.name, `${compEntry.name}.a2ui.json`);
|
|
58
|
+
try {
|
|
59
|
+
const raw = await fs.readFile(a2uiPath, 'utf8');
|
|
60
|
+
const data = JSON.parse(raw);
|
|
61
|
+
if (data?.title) _componentData.set(data.title, data);
|
|
62
|
+
} catch {
|
|
63
|
+
// No .a2ui.json for this component — skip
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function _loadBrowser() {
|
|
69
|
+
if (!_globA2UIModules) return;
|
|
70
|
+
|
|
71
|
+
for (const [, loader] of Object.entries(_globA2UIModules)) {
|
|
72
|
+
try {
|
|
73
|
+
const raw = await loader();
|
|
74
|
+
const data = JSON.parse(raw);
|
|
75
|
+
if (data?.title) _componentData.set(data.title, data);
|
|
76
|
+
} catch {
|
|
77
|
+
// skip malformed .a2ui.json
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function loadCatalog() {
|
|
83
|
+
if (_loaded) return;
|
|
84
|
+
if (_loadPromise) return _loadPromise;
|
|
85
|
+
_loadPromise = (async () => {
|
|
86
|
+
if (IS_NODE) await _loadNode();
|
|
87
|
+
else await _loadBrowser();
|
|
88
|
+
})();
|
|
89
|
+
await _loadPromise;
|
|
90
|
+
_loaded = true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Eager load on module import so the synchronous getters below are safe.
|
|
94
|
+
await loadCatalog();
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get `.a2ui.json` data for a named component.
|
|
98
|
+
* @param {string} name — PascalCase component name (e.g., "Swiper")
|
|
99
|
+
* @returns {object|null} raw .a2ui.json document, or null
|
|
100
|
+
*/
|
|
101
|
+
export function getComponentData(name) {
|
|
102
|
+
return _componentData.get(name) || null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get all loaded `.a2ui.json` component data.
|
|
107
|
+
* @returns {Object.<string, object>} component name → .a2ui.json data
|
|
108
|
+
*/
|
|
109
|
+
export function getAllComponentData() {
|
|
110
|
+
return Object.fromEntries(_componentData);
|
|
111
|
+
}
|
package/context-assembler.js
CHANGED
|
@@ -11,7 +11,11 @@ import { getCatalog, getComponentsByCategory } from './catalog.js';
|
|
|
11
11
|
import { serializeEntry } from './component-entry.js';
|
|
12
12
|
import { classifyIntent, getDomain } from './domain-router.js';
|
|
13
13
|
import { getAntiPatterns } from './anti-patterns.js';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
searchAll as searchCompositions,
|
|
16
|
+
getComposition,
|
|
17
|
+
getAllCompositions,
|
|
18
|
+
} from '../compose/strategies/zettel/composition-library.js';
|
|
15
19
|
|
|
16
20
|
/** Token budget per tier */
|
|
17
21
|
const TIER_BUDGETS = {
|
|
@@ -125,28 +129,30 @@ export async function assembleContext({ intent = '', tier = 1, domain: overrideD
|
|
|
125
129
|
}
|
|
126
130
|
}
|
|
127
131
|
|
|
128
|
-
// ── Tier 3+: Add matching
|
|
132
|
+
// ── Tier 3+: Add matching compositions ──
|
|
133
|
+
// §64 migrated from pattern-library to composition-library. Compositions
|
|
134
|
+
// are the canonical retrieval surface post-§65.
|
|
129
135
|
if (tier >= 3) {
|
|
130
|
-
const
|
|
131
|
-
?
|
|
132
|
-
:
|
|
136
|
+
const matchedCompositions = intent
|
|
137
|
+
? searchCompositions(intent).map((hit) => getComposition(hit.name)).filter(Boolean)
|
|
138
|
+
: getAllCompositions().filter((c) => c.domain === domainName);
|
|
133
139
|
|
|
134
|
-
for (const
|
|
135
|
-
const cost = estimateTokens(
|
|
140
|
+
for (const composition of matchedCompositions) {
|
|
141
|
+
const cost = estimateTokens(composition);
|
|
136
142
|
if (usedTokens + cost > budget) break;
|
|
137
|
-
result.patterns.push(
|
|
143
|
+
result.patterns.push(composition);
|
|
138
144
|
usedTokens += cost;
|
|
139
145
|
}
|
|
140
146
|
}
|
|
141
147
|
|
|
142
|
-
// ── Tier 4: Full exploration — add remaining
|
|
148
|
+
// ── Tier 4: Full exploration — add remaining compositions ──
|
|
143
149
|
if (tier >= 4) {
|
|
144
|
-
const
|
|
145
|
-
for (const
|
|
146
|
-
if (
|
|
147
|
-
const cost = estimateTokens(
|
|
150
|
+
const added = new Set(result.patterns.map((c) => c.name));
|
|
151
|
+
for (const composition of getAllCompositions()) {
|
|
152
|
+
if (added.has(composition.name)) continue;
|
|
153
|
+
const cost = estimateTokens(composition);
|
|
148
154
|
if (usedTokens + cost > budget) break;
|
|
149
|
-
result.patterns.push(
|
|
155
|
+
result.patterns.push(composition);
|
|
150
156
|
usedTokens += cost;
|
|
151
157
|
}
|
|
152
158
|
}
|
package/embedding/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @adia-ai/a2ui-retrieval/embedding — embedding-provider +
|
|
2
|
+
* @adia-ai/a2ui-retrieval/embedding — embedding-provider + chunk retriever surface.
|
|
3
|
+
*
|
|
4
|
+
* Since §65 (v0.4.7), the legacy pattern-embedding retriever was retired
|
|
5
|
+
* along with its only consumer (concept-mapper.js, dead post-§64). The
|
|
6
|
+
* canonical retrieval surface post-§40 is chunk-embedding-retriever.
|
|
3
7
|
*/
|
|
4
8
|
|
|
5
9
|
export * from './embedding-provider.js';
|
|
6
|
-
export * from './embedding-retriever.js';
|
|
7
10
|
export * from './chunk-embedding-retriever.js';
|
package/index.js
CHANGED
|
@@ -10,7 +10,7 @@ export { isConversational } from './intent/intent-gate.js';
|
|
|
10
10
|
export { assessClarity } from './intent/clarity.js';
|
|
11
11
|
export { detectReferences, researchIntent } from './authoring/web-research.js';
|
|
12
12
|
export { assembleContext } from './context-assembler.js';
|
|
13
|
-
export {
|
|
13
|
+
export { getComponentData, getAllComponentData } from './component-catalog.js';
|
|
14
14
|
export { extractExpectations, verifyAlignment, checkIntentAlignment } from './intent/intent-alignment.js';
|
|
15
15
|
export { decomposeIntent, composeSubtasks } from './intent/decomposer.js';
|
|
16
16
|
export { getWiringCatalog, getControllerInfo, getHandlerInfo } from './wiring-catalog.js';
|
package/intent/clarity.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { classifyIntent } from '../domain-router.js';
|
|
16
|
-
import {
|
|
16
|
+
import { searchAll as searchCompositions } from '../../compose/strategies/zettel/composition-library.js';
|
|
17
17
|
|
|
18
18
|
// ── Dimension detectors ──────────────────────────────────────────────────
|
|
19
19
|
|
|
@@ -81,7 +81,7 @@ export function assessClarity(intent, classification) {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
const cls = classification || classifyIntent(text);
|
|
84
|
-
const
|
|
84
|
+
const compositionMatches = searchCompositions(text);
|
|
85
85
|
const lower = text.toLowerCase();
|
|
86
86
|
const wordCount = text.split(/\s+/).length;
|
|
87
87
|
|
|
@@ -115,8 +115,8 @@ export function assessClarity(intent, classification) {
|
|
|
115
115
|
actionScore * 0.15
|
|
116
116
|
);
|
|
117
117
|
|
|
118
|
-
// ── Bonus:
|
|
119
|
-
const patternBonus =
|
|
118
|
+
// ── Bonus: composition match adds confidence ──
|
|
119
|
+
const patternBonus = compositionMatches.length > 0 ? 0.15 : 0;
|
|
120
120
|
// Bonus: long intents are usually more specific
|
|
121
121
|
const lengthBonus = wordCount > 10 ? 0.1 : wordCount > 6 ? 0.05 : 0;
|
|
122
122
|
|
|
@@ -38,28 +38,36 @@
|
|
|
38
38
|
let _vocab = null;
|
|
39
39
|
async function getVocab() {
|
|
40
40
|
if (_vocab) return _vocab;
|
|
41
|
-
let
|
|
41
|
+
let catalogJson = {};
|
|
42
42
|
try {
|
|
43
43
|
const IS_NODE = typeof process !== 'undefined' && process.versions?.node;
|
|
44
44
|
if (IS_NODE) {
|
|
45
45
|
const fs = await import(/* @vite-ignore */ 'node:fs/promises');
|
|
46
46
|
const path = await import(/* @vite-ignore */ 'node:path');
|
|
47
47
|
const url = await import(/* @vite-ignore */ 'node:url');
|
|
48
|
-
//
|
|
48
|
+
// Since §65 (v0.4.7), reads from canonical v0.9 catalog at
|
|
49
|
+
// corpus/catalog-a2ui_0_9.json (assembled from yamls). Previously read
|
|
50
|
+
// the hand-maintained corpus/patterns/_components.json retired in §65.
|
|
51
|
+
// packages/a2ui/retrieval/intent → up 2 → packages/a2ui → corpus/catalog-a2ui_0_9.json
|
|
49
52
|
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
50
|
-
const raw = await fs.readFile(path.join(__dirname, '../../corpus/
|
|
51
|
-
|
|
53
|
+
const raw = await fs.readFile(path.join(__dirname, '../../corpus/catalog-a2ui_0_9.json'), 'utf8');
|
|
54
|
+
catalogJson = JSON.parse(raw);
|
|
52
55
|
} else {
|
|
53
|
-
const resp = await fetch(new URL('../../corpus/
|
|
54
|
-
if (resp.ok)
|
|
56
|
+
const resp = await fetch(new URL('../../corpus/catalog-a2ui_0_9.json', import.meta.url));
|
|
57
|
+
if (resp.ok) catalogJson = await resp.json();
|
|
55
58
|
}
|
|
56
59
|
} catch { /* empty vocab — analyzer will still work, just unconstrained */ }
|
|
57
60
|
|
|
61
|
+
// Adapt v0.9 catalog shape: `components: {Name: {x-adiaui: {synonyms: {tags: [...]}}}}`
|
|
62
|
+
// → legacy `{Name: {aliases}}` shape this vocab builder expects.
|
|
63
|
+
const comps = catalogJson?.components || {};
|
|
58
64
|
const displayList = [];
|
|
59
65
|
const allowedSet = new Set();
|
|
60
|
-
for (const [name,
|
|
66
|
+
for (const [name, def] of Object.entries(comps)) {
|
|
61
67
|
allowedSet.add(name);
|
|
62
|
-
const
|
|
68
|
+
const ext = def?.['x-adiaui'] || {};
|
|
69
|
+
const syns = (ext.synonyms && typeof ext.synonyms === 'object') ? ext.synonyms : null;
|
|
70
|
+
const aliases = Array.isArray(syns?.tags) ? syns.tags : [];
|
|
63
71
|
for (const a of aliases) allowedSet.add(a);
|
|
64
72
|
displayList.push(aliases.length ? `${name} (also: ${aliases.join(', ')})` : name);
|
|
65
73
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adia-ai/a2ui-retrieval",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
4
4
|
"description": "AdiaUI A2UI retrieval layer — catalog lookup, intent classification, domain routing, pattern + anti-pattern matching, clarity + context assembly. Consumed by the compose engine and any A2UI-protocol tooling that needs to reason about user intent against the catalog.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pattern Promotion — Automatically identifies generations that should
|
|
3
|
-
* become reusable patterns based on quality signals and user feedback.
|
|
4
|
-
*
|
|
5
|
-
* Promotion criteria (all must be met):
|
|
6
|
-
* - Generation score >= 95
|
|
7
|
-
* - Feedback rating >= 4 (if feedback exists)
|
|
8
|
-
* - Intent alignment score >= 0.8 (if checked)
|
|
9
|
-
* - User didn't heavily edit the output
|
|
10
|
-
* - Explicit shouldBePattern flag (from feedback) OR auto-detected
|
|
11
|
-
*
|
|
12
|
-
* Auto-detection: a generation is auto-promoted if it scores high on all
|
|
13
|
-
* dimensions and uses a novel combination of components not already in
|
|
14
|
-
* the pattern library.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { getAllPatterns } from '../pattern-library.js';
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Evaluate whether a generation should be promoted to a named pattern.
|
|
21
|
-
*
|
|
22
|
-
* @param {object} opts
|
|
23
|
-
* @param {number} opts.score — Validation score (0-100)
|
|
24
|
-
* @param {object} [opts.feedback] — User feedback { rating, userEdited }
|
|
25
|
-
* @param {object} [opts.alignment] — Intent alignment { score }
|
|
26
|
-
* @param {string[]} opts.componentTypes — Component types used in generation
|
|
27
|
-
* @param {string} opts.intent — Original intent
|
|
28
|
-
* @returns {{ shouldPromote: boolean, confidence: number, reason: string, suggestedName: string }}
|
|
29
|
-
*/
|
|
30
|
-
export function evaluatePromotion({ score, feedback, alignment, componentTypes, intent }) {
|
|
31
|
-
let confidence = 0;
|
|
32
|
-
const reasons = [];
|
|
33
|
-
|
|
34
|
-
// ── Hard gates ──
|
|
35
|
-
if (score < 95) {
|
|
36
|
-
return { shouldPromote: false, confidence: 0, reason: `Score ${score} < 95`, suggestedName: '' };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Score contribution (0.3 weight)
|
|
40
|
-
confidence += (score / 100) * 0.3;
|
|
41
|
-
reasons.push(`score: ${score}/100`);
|
|
42
|
-
|
|
43
|
-
// ── Feedback signals (0.3 weight) ──
|
|
44
|
-
if (feedback) {
|
|
45
|
-
if (feedback.rating >= 4) {
|
|
46
|
-
confidence += (feedback.rating / 5) * 0.2;
|
|
47
|
-
reasons.push(`rating: ${feedback.rating}/5`);
|
|
48
|
-
} else if (feedback.rating > 0) {
|
|
49
|
-
return { shouldPromote: false, confidence, reason: `Rating ${feedback.rating} < 4`, suggestedName: '' };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (feedback.userEdited) {
|
|
53
|
-
confidence -= 0.15;
|
|
54
|
-
reasons.push('user edited (penalty)');
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (feedback.shouldBePattern) {
|
|
58
|
-
confidence += 0.15;
|
|
59
|
-
reasons.push('user flagged as pattern');
|
|
60
|
-
}
|
|
61
|
-
} else {
|
|
62
|
-
// No feedback — partial confidence from score alone
|
|
63
|
-
confidence += 0.1;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// ── Alignment signals (0.2 weight) ──
|
|
67
|
-
if (alignment) {
|
|
68
|
-
if (alignment.score >= 0.8) {
|
|
69
|
-
confidence += alignment.score * 0.2;
|
|
70
|
-
reasons.push(`alignment: ${Math.round(alignment.score * 100)}%`);
|
|
71
|
-
} else {
|
|
72
|
-
confidence -= 0.1;
|
|
73
|
-
reasons.push(`low alignment: ${Math.round(alignment.score * 100)}%`);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// ── Novelty bonus (0.2 weight) ──
|
|
78
|
-
const novelty = assessNovelty(componentTypes);
|
|
79
|
-
if (novelty > 0.5) {
|
|
80
|
-
confidence += novelty * 0.2;
|
|
81
|
-
reasons.push(`novelty: ${Math.round(novelty * 100)}%`);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// ── Decision ──
|
|
85
|
-
const shouldPromote = confidence >= 0.5;
|
|
86
|
-
const suggestedName = generatePatternName(intent);
|
|
87
|
-
|
|
88
|
-
return {
|
|
89
|
-
shouldPromote,
|
|
90
|
-
confidence: Math.round(confidence * 100) / 100,
|
|
91
|
-
reason: reasons.join(', '),
|
|
92
|
-
suggestedName,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Assess how novel a component combination is compared to existing patterns.
|
|
98
|
-
* Higher = more novel (not well-covered by existing patterns).
|
|
99
|
-
*
|
|
100
|
-
* @param {string[]} componentTypes
|
|
101
|
-
* @returns {number} — 0 (duplicate) to 1 (completely novel)
|
|
102
|
-
*/
|
|
103
|
-
function assessNovelty(componentTypes) {
|
|
104
|
-
const existing = getAllPatterns();
|
|
105
|
-
if (existing.length === 0) return 1;
|
|
106
|
-
|
|
107
|
-
const typeSet = new Set(componentTypes);
|
|
108
|
-
let bestOverlap = 0;
|
|
109
|
-
|
|
110
|
-
for (const pattern of existing) {
|
|
111
|
-
const patternTypes = new Set(pattern.components || []);
|
|
112
|
-
const intersection = [...typeSet].filter(t => patternTypes.has(t)).length;
|
|
113
|
-
const union = new Set([...typeSet, ...patternTypes]).size;
|
|
114
|
-
const jaccard = union > 0 ? intersection / union : 0;
|
|
115
|
-
bestOverlap = Math.max(bestOverlap, jaccard);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Invert: high overlap = low novelty
|
|
119
|
-
return 1 - bestOverlap;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Generate a suggested pattern name from an intent string.
|
|
124
|
-
*
|
|
125
|
-
* @param {string} intent
|
|
126
|
-
* @returns {string}
|
|
127
|
-
*/
|
|
128
|
-
function generatePatternName(intent) {
|
|
129
|
-
return (intent || 'unnamed')
|
|
130
|
-
.toLowerCase()
|
|
131
|
-
.replace(/\b(create|build|make|show|add|generate|a|an|the|with|and|for|of)\b/g, '')
|
|
132
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
133
|
-
.replace(/^-|-$/g, '')
|
|
134
|
-
.slice(0, 40) || 'unnamed-pattern';
|
|
135
|
-
}
|