@forumone/throughline-components 0.0.1 → 0.2.0

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.
Files changed (68) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +117 -28
  4. package/dist/index.d.ts +3 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +2 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/manifest-source.d.ts +17 -0
  9. package/dist/manifest-source.d.ts.map +1 -0
  10. package/dist/manifest-source.js +60 -0
  11. package/dist/manifest-source.js.map +1 -0
  12. package/dist/matching/index.d.ts +3 -0
  13. package/dist/matching/index.d.ts.map +1 -0
  14. package/dist/matching/index.js +2 -0
  15. package/dist/matching/index.js.map +1 -0
  16. package/dist/matching/tfidf.d.ts +13 -0
  17. package/dist/matching/tfidf.d.ts.map +1 -0
  18. package/dist/matching/tfidf.js +118 -0
  19. package/dist/matching/tfidf.js.map +1 -0
  20. package/dist/matching/types.d.ts +22 -0
  21. package/dist/matching/types.d.ts.map +1 -0
  22. package/dist/matching/types.js +2 -0
  23. package/dist/matching/types.js.map +1 -0
  24. package/dist/options.d.ts +125 -0
  25. package/dist/options.d.ts.map +1 -0
  26. package/dist/options.js +42 -0
  27. package/dist/options.js.map +1 -0
  28. package/dist/plugin.d.ts +4 -0
  29. package/dist/plugin.d.ts.map +1 -0
  30. package/dist/plugin.js +83 -0
  31. package/dist/plugin.js.map +1 -0
  32. package/dist/tools/find-anti-pattern.d.ts +9 -0
  33. package/dist/tools/find-anti-pattern.d.ts.map +1 -0
  34. package/dist/tools/find-anti-pattern.js +37 -0
  35. package/dist/tools/find-anti-pattern.js.map +1 -0
  36. package/dist/tools/get-contract.d.ts +4 -0
  37. package/dist/tools/get-contract.d.ts.map +1 -0
  38. package/dist/tools/get-contract.js +19 -0
  39. package/dist/tools/get-contract.js.map +1 -0
  40. package/dist/tools/get-tokens.d.ts +4 -0
  41. package/dist/tools/get-tokens.d.ts.map +1 -0
  42. package/dist/tools/get-tokens.js +19 -0
  43. package/dist/tools/get-tokens.js.map +1 -0
  44. package/dist/tools/get-variants.d.ts +4 -0
  45. package/dist/tools/get-variants.d.ts.map +1 -0
  46. package/dist/tools/get-variants.js +19 -0
  47. package/dist/tools/get-variants.js.map +1 -0
  48. package/dist/tools/index.d.ts +8 -0
  49. package/dist/tools/index.d.ts.map +1 -0
  50. package/dist/tools/index.js +8 -0
  51. package/dist/tools/index.js.map +1 -0
  52. package/dist/tools/list-components.d.ts +4 -0
  53. package/dist/tools/list-components.d.ts.map +1 -0
  54. package/dist/tools/list-components.js +25 -0
  55. package/dist/tools/list-components.js.map +1 -0
  56. package/dist/tools/suggest-for-intent.d.ts +12 -0
  57. package/dist/tools/suggest-for-intent.d.ts.map +1 -0
  58. package/dist/tools/suggest-for-intent.js +86 -0
  59. package/dist/tools/suggest-for-intent.js.map +1 -0
  60. package/dist/tools/validate-composition.d.ts +9 -0
  61. package/dist/tools/validate-composition.d.ts.map +1 -0
  62. package/dist/tools/validate-composition.js +37 -0
  63. package/dist/tools/validate-composition.js.map +1 -0
  64. package/dist/validation/composition.d.ts +38 -0
  65. package/dist/validation/composition.d.ts.map +1 -0
  66. package/dist/validation/composition.js +135 -0
  67. package/dist/validation/composition.js.map +1 -0
  68. package/package.json +62 -7
package/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # @forumone/throughline-components
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#16](https://github.com/forumone/throughline/pull/16) [`4db5168`](https://github.com/forumone/throughline/commit/4db5168cfe83922ad371b7927029c21b009b1e53) Thanks [@briangraves](https://github.com/briangraves)! - Initial release. MCP server that exposes a design system manifest as seven conversational tools: `list_components`, `get_contract`, `get_variants`, `get_tokens`, `suggest_for_intent`, `validate_composition`, `find_anti_pattern`. Ships TF-IDF intent matching (no external deps); embeddings strategy reserved for a follow-up release. Accepts manifests as imported objects, remote URLs (with `refreshInterval`), or Payload collections.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Forum One Communications Corporation
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,45 +1,134 @@
1
1
  # @forumone/throughline-components
2
2
 
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
3
+ MCP server that exposes a design system manifest as conversational primitives. Drop it into a Payload + Throughline app and Claude can list components, read contracts, suggest components for an intent, validate compositions, and surface anti-patterns — against any design system that satisfies the Throughline contract.
4
4
 
5
- **This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
5
+ ## What this package provides
6
6
 
7
- This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
7
+ Seven MCP tools served at `${routePrefix}/mcp` (default `/api/components/mcp`):
8
8
 
9
- ## Purpose
9
+ | Tool | Purpose |
10
+ |---|---|
11
+ | `list_components` | Discover what components exist (optionally filtered by category) |
12
+ | `get_contract` | Full `ComponentContract` for a named component |
13
+ | `get_variants` | Available variants and when to use each |
14
+ | `get_tokens` | Tokens a component consumes + configurable token-backed props |
15
+ | `suggest_for_intent` | Ranked component recommendations for a natural-language intent |
16
+ | `validate_composition` | Errors + warnings for a proposed block list against the design system's rules |
17
+ | `find_anti_pattern` | Surfaces structural anti-patterns (multiple Heroes, Hero at the bottom, etc.) |
10
18
 
11
- This package exists to:
12
- 1. Configure OIDC trusted publishing for the package name `@forumone/throughline-components`
13
- 2. Enable secure, token-less publishing from CI/CD workflows
14
- 3. Establish provenance for packages published under this name
19
+ Every consequential call writes to the audit log via `@forumone/throughline-core`'s audit writer.
15
20
 
16
- ## What is OIDC Trusted Publishing?
21
+ ## Installation
17
22
 
18
- OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
23
+ ```bash
24
+ pnpm add @forumone/throughline-components
25
+ ```
19
26
 
20
- ## Setup Instructions
27
+ Peer: `payload@^3.0.0`. Required runtime: `@forumone/throughline-core` (the components plugin asserts the `audit-log` capability at init).
21
28
 
22
- To properly configure OIDC trusted publishing for this package:
29
+ ## Usage
23
30
 
24
- 1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
25
- 2. Configure the trusted publisher (e.g., GitHub Actions)
26
- 3. Specify the repository and workflow that should be allowed to publish
27
- 4. Use the configured workflow to publish your actual package
31
+ ```ts
32
+ import { buildConfig } from 'payload'
33
+ import { auditPlugin, createInngestClient } from '@forumone/throughline-core'
34
+ import { componentsPlugin } from '@forumone/throughline-components'
35
+ import manifest from '@my-company/design-system/manifest' with { type: 'json' }
28
36
 
29
- ## DO NOT USE THIS PACKAGE
37
+ const inngest = createInngestClient({ id: 'my-site' })
30
38
 
31
- This package is a placeholder for OIDC configuration only. It:
32
- - Contains no executable code
33
- - Provides no functionality
34
- - Should not be installed as a dependency
35
- - Exists only for administrative purposes
39
+ export default buildConfig({
40
+ // collections, db, secret...
41
+ plugins: [
42
+ auditPlugin({ inngest }), // must come first (componentsPlugin requires audit-log)
43
+ componentsPlugin({
44
+ manifest: { type: 'object', manifest },
45
+ matching: { strategy: 'tfidf' },
46
+ }),
47
+ ],
48
+ })
49
+ ```
36
50
 
37
- ## More Information
51
+ ## Manifest sources
38
52
 
39
- For more details about npm's trusted publishing feature, see:
40
- - [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
41
- - [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
53
+ Three ways to supply the manifest:
42
54
 
43
- ---
55
+ **Imported object** — simplest; bundle the manifest with the app.
44
56
 
45
- **Maintained for OIDC setup purposes only**
57
+ ```ts
58
+ componentsPlugin({ manifest: { type: 'object', manifest: importedManifest } })
59
+ ```
60
+
61
+ **Remote URL** — for design systems deployed independently.
62
+
63
+ ```ts
64
+ componentsPlugin({
65
+ manifest: {
66
+ type: 'url',
67
+ url: 'https://design-system.example.com/manifest.json',
68
+ refreshInterval: 3600, // seconds; manifest is re-fetched after the TTL expires
69
+ },
70
+ })
71
+ ```
72
+
73
+ **Payload collection** — when the manifest is admin-editable.
74
+
75
+ ```ts
76
+ componentsPlugin({
77
+ manifest: { type: 'payload-collection', slug: 'design-system-manifest' },
78
+ })
79
+ ```
80
+
81
+ The loader reads the most recent document by `updatedAt`, optionally filtered by `documentId`, and looks for a `data` field on it before falling back to the document itself.
82
+
83
+ ## Intent matching
84
+
85
+ The current shipping strategy is **TF-IDF**: zero external dependencies, weights `intent` more than `description`, fast enough that the first response after deploy is sub-100ms.
86
+
87
+ ```ts
88
+ componentsPlugin({
89
+ manifest: { /* ... */ },
90
+ matching: { strategy: 'tfidf', maxRecommendations: 5 },
91
+ })
92
+ ```
93
+
94
+ An embeddings-based matcher is on the roadmap. The matcher interface is strategy-agnostic, so swapping it in won't change the tool surface. Until then, real recommendation quality depends on contract authoring — specifically, on how vividly each `intent` field describes the component's editorial purpose.
95
+
96
+ ## Composition rules enforced
97
+
98
+ `validate_composition` checks:
99
+
100
+ - **`forbiddenAdjacent`** — neither the previous nor next block can be the forbidden type
101
+ - **`maxPerPage`** — total occurrences in the block list don't exceed the contract's `maxPerPage`
102
+ - **`requiredSiblings`** — warns (not errors) when a sibling that the contract expects isn't present
103
+ - **Unknown components** — flagged as an error
104
+ - **Unknown variants** — flagged as an error per block
105
+
106
+ `find_anti_pattern` surfaces:
107
+
108
+ - "Multiple X" labels when X appears more than once in the block list
109
+ - "End of page" / "bottom" labels when an X with `placement: ['page']` lands as the last block
110
+
111
+ The two tools are complementary. Use `validate_composition` to gate publishing and `find_anti_pattern` to surface design quality issues that aren't strictly invalid.
112
+
113
+ ## The `_meta` parameter
114
+
115
+ `suggest_for_intent`, `validate_composition`, and `find_anti_pattern` all accept the framework's `_meta` payload (via `withMeta` from `@forumone/throughline-core`):
116
+
117
+ ```jsonc
118
+ {
119
+ "intent": "introduce a new climate fellowship",
120
+ "_meta": {
121
+ "userPrompt": "I want a hero for the new climate program",
122
+ "reasoning": "The marketer asked for a page opener, not a section header",
123
+ "changesSummary": "Recommended Hero (split variant) for the program landing"
124
+ }
125
+ }
126
+ ```
127
+
128
+ The audit writer reads these fields and stores them on every record so "why did Claude recommend Hero for the contact page?" is answerable later.
129
+
130
+ ## Related packages
131
+
132
+ - `@forumone/throughline-design-contract` — the manifest schema this plugin reads
133
+ - `@forumone/throughline-reference-ds` — reference DS used as a test fixture and a starting point for clients
134
+ - `@forumone/throughline-core` — required peer; provides the audit log, MCP handler, and `withMeta` helper this plugin builds on
@@ -0,0 +1,3 @@
1
+ export { componentsPlugin } from './plugin.js';
2
+ export type { ComponentsPluginOptions, ManifestSource, MatchingConfig } from './options.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAC9C,YAAY,EAAE,uBAAuB,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { componentsPlugin } from './plugin.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA"}
@@ -0,0 +1,17 @@
1
+ import type { Payload } from 'payload';
2
+ import { type LoadedManifest } from '@forumone/throughline-design-contract';
3
+ import type { ManifestSource } from './options.js';
4
+ export interface ManifestLoader {
5
+ /** Returns the cached manifest, refreshing if the URL TTL has expired. */
6
+ get(): Promise<LoadedManifest>;
7
+ /** Forces a re-fetch from the source. */
8
+ refresh(): Promise<LoadedManifest>;
9
+ }
10
+ /**
11
+ * Builds a manifest loader from a source descriptor. The loader caches the
12
+ * parsed manifest indefinitely for `object` and `payload-collection` sources;
13
+ * for `url` sources it honors `refreshInterval` (seconds) and re-fetches when
14
+ * the cached copy is older than that.
15
+ */
16
+ export declare function createManifestLoader(source: ManifestSource, payload: Payload): ManifestLoader;
17
+ //# sourceMappingURL=manifest-source.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest-source.d.ts","sourceRoot":"","sources":["../src/manifest-source.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EACL,KAAK,cAAc,EAGpB,MAAM,uCAAuC,CAAA;AAC9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAElD,MAAM,WAAW,cAAc;IAC7B,0EAA0E;IAC1E,GAAG,IAAI,OAAO,CAAC,cAAc,CAAC,CAAA;IAC9B,yCAAyC;IACzC,OAAO,IAAI,OAAO,CAAC,cAAc,CAAC,CAAA;CACnC;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,OAAO,GAAG,cAAc,CAuD7F"}
@@ -0,0 +1,60 @@
1
+ import { loadManifest, loadManifestFromUrl, } from '@forumone/throughline-design-contract';
2
+ /**
3
+ * Builds a manifest loader from a source descriptor. The loader caches the
4
+ * parsed manifest indefinitely for `object` and `payload-collection` sources;
5
+ * for `url` sources it honors `refreshInterval` (seconds) and re-fetches when
6
+ * the cached copy is older than that.
7
+ */
8
+ export function createManifestLoader(source, payload) {
9
+ let cached = null;
10
+ let lastLoadedAt = 0;
11
+ async function load() {
12
+ switch (source.type) {
13
+ case 'object':
14
+ return loadManifest(source.manifest);
15
+ case 'url':
16
+ return loadManifestFromUrl(source.url);
17
+ case 'payload-collection': {
18
+ const where = source.documentId
19
+ ? { id: { equals: source.documentId } }
20
+ : undefined;
21
+ const result = await payload.find({
22
+ collection: source.slug,
23
+ ...(where ? { where } : {}),
24
+ limit: 1,
25
+ sort: '-updatedAt',
26
+ });
27
+ const doc = result.docs[0];
28
+ if (!doc) {
29
+ const idClause = source.documentId ? ` with id "${source.documentId}"` : '';
30
+ throw new Error(`No manifest document found in collection "${source.slug}"${idClause}`);
31
+ }
32
+ const candidate = (doc['data'] ?? doc);
33
+ return loadManifest(candidate);
34
+ }
35
+ }
36
+ }
37
+ return {
38
+ async get() {
39
+ if (cached) {
40
+ if (source.type === 'url' && source.refreshInterval !== undefined) {
41
+ const ageMs = Date.now() - lastLoadedAt;
42
+ if (ageMs > source.refreshInterval * 1000) {
43
+ cached = await load();
44
+ lastLoadedAt = Date.now();
45
+ }
46
+ }
47
+ return cached;
48
+ }
49
+ cached = await load();
50
+ lastLoadedAt = Date.now();
51
+ return cached;
52
+ },
53
+ async refresh() {
54
+ cached = await load();
55
+ lastLoadedAt = Date.now();
56
+ return cached;
57
+ },
58
+ };
59
+ }
60
+ //# sourceMappingURL=manifest-source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest-source.js","sourceRoot":"","sources":["../src/manifest-source.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,YAAY,EACZ,mBAAmB,GACpB,MAAM,uCAAuC,CAAA;AAU9C;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAAsB,EAAE,OAAgB;IAC3E,IAAI,MAAM,GAA0B,IAAI,CAAA;IACxC,IAAI,YAAY,GAAG,CAAC,CAAA;IAEpB,KAAK,UAAU,IAAI;QACjB,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;YACpB,KAAK,QAAQ;gBACX,OAAO,YAAY,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;YACtC,KAAK,KAAK;gBACR,OAAO,mBAAmB,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;YACxC,KAAK,oBAAoB,CAAC,CAAC,CAAC;gBAC1B,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU;oBAC7B,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,UAAU,EAAE,EAAE;oBACvC,CAAC,CAAC,SAAS,CAAA;gBACb,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC;oBAChC,UAAU,EAAE,MAAM,CAAC,IAAI;oBACvB,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;oBAC3B,KAAK,EAAE,CAAC;oBACR,IAAI,EAAE,YAAY;iBACnB,CAAC,CAAA;gBACF,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAwC,CAAA;gBACjE,IAAI,CAAC,GAAG,EAAE,CAAC;oBACT,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,aAAa,MAAM,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,EAAE,CAAA;oBAC3E,MAAM,IAAI,KAAK,CACb,6CAA6C,MAAM,CAAC,IAAI,IAAI,QAAQ,EAAE,CACvE,CAAA;gBACH,CAAC;gBACD,MAAM,SAAS,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,GAAG,CAAY,CAAA;gBACjD,OAAO,YAAY,CAAC,SAAS,CAAC,CAAA;YAChC,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO;QACL,KAAK,CAAC,GAAG;YACP,IAAI,MAAM,EAAE,CAAC;gBACX,IAAI,MAAM,CAAC,IAAI,KAAK,KAAK,IAAI,MAAM,CAAC,eAAe,KAAK,SAAS,EAAE,CAAC;oBAClE,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,YAAY,CAAA;oBACvC,IAAI,KAAK,GAAG,MAAM,CAAC,eAAe,GAAG,IAAI,EAAE,CAAC;wBAC1C,MAAM,GAAG,MAAM,IAAI,EAAE,CAAA;wBACrB,YAAY,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;oBAC3B,CAAC;gBACH,CAAC;gBACD,OAAO,MAAM,CAAA;YACf,CAAC;YACD,MAAM,GAAG,MAAM,IAAI,EAAE,CAAA;YACrB,YAAY,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;YACzB,OAAO,MAAM,CAAA;QACf,CAAC;QACD,KAAK,CAAC,OAAO;YACX,MAAM,GAAG,MAAM,IAAI,EAAE,CAAA;YACrB,YAAY,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;YACzB,OAAO,MAAM,CAAA;QACf,CAAC;KACF,CAAA;AACH,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { createTfidfMatcher } from './tfidf.js';
2
+ export type { Matcher, RankedComponent, RankedSuggestion } from './types.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/matching/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAC/C,YAAY,EAAE,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA"}
@@ -0,0 +1,2 @@
1
+ export { createTfidfMatcher } from './tfidf.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/matching/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA"}
@@ -0,0 +1,13 @@
1
+ import type { ComponentContract } from '@forumone/throughline-design-contract';
2
+ import type { Matcher } from './types.js';
3
+ /**
4
+ * Simple TF-IDF matcher over component search documents. Indexed once at
5
+ * factory time; subsequent `rank()` calls scan the precomputed index.
6
+ *
7
+ * The search document weights `intent` more than `description`, and includes
8
+ * variant `whenToUse` text + example intents. Editorial language often shows
9
+ * up in those fields, so weighting them helps real intents like "introduce
10
+ * a new program" find the Hero's `intent` rather than its name.
11
+ */
12
+ export declare function createTfidfMatcher(components: ComponentContract[]): Matcher;
13
+ //# sourceMappingURL=tfidf.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tfidf.d.ts","sourceRoot":"","sources":["../../src/matching/tfidf.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAA;AAC9E,OAAO,KAAK,EAAE,OAAO,EAAmB,MAAM,YAAY,CAAA;AAE1D;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,iBAAiB,EAAE,GAAG,OAAO,CA8C3E"}
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Simple TF-IDF matcher over component search documents. Indexed once at
3
+ * factory time; subsequent `rank()` calls scan the precomputed index.
4
+ *
5
+ * The search document weights `intent` more than `description`, and includes
6
+ * variant `whenToUse` text + example intents. Editorial language often shows
7
+ * up in those fields, so weighting them helps real intents like "introduce
8
+ * a new program" find the Hero's `intent` rather than its name.
9
+ */
10
+ export function createTfidfMatcher(components) {
11
+ const index = components.map((component) => {
12
+ const document = makeSearchDocument(component);
13
+ const tokens = tokenize(document);
14
+ const termFreq = new Map();
15
+ for (const token of tokens) {
16
+ termFreq.set(token, (termFreq.get(token) ?? 0) + 1);
17
+ }
18
+ return { component, terms: termFreq, length: Math.max(tokens.length, 1) };
19
+ });
20
+ const docCount = index.length;
21
+ const docFreq = new Map();
22
+ for (const entry of index) {
23
+ for (const term of entry.terms.keys()) {
24
+ docFreq.set(term, (docFreq.get(term) ?? 0) + 1);
25
+ }
26
+ }
27
+ const idf = new Map();
28
+ for (const [term, df] of docFreq) {
29
+ idf.set(term, Math.log((docCount + 1) / (df + 1)) + 1);
30
+ }
31
+ return {
32
+ rank(query) {
33
+ const queryCounts = new Map();
34
+ for (const token of tokenize(query)) {
35
+ queryCounts.set(token, (queryCounts.get(token) ?? 0) + 1);
36
+ }
37
+ const scored = index.map((entry) => {
38
+ let score = 0;
39
+ for (const [term, queryCount] of queryCounts) {
40
+ const docCountForTerm = entry.terms.get(term) ?? 0;
41
+ if (docCountForTerm === 0)
42
+ continue;
43
+ const tf = docCountForTerm / entry.length;
44
+ const termIdf = idf.get(term) ?? 0;
45
+ score += tf * termIdf * queryCount;
46
+ }
47
+ return { component: entry.component, score };
48
+ });
49
+ scored.sort((a, b) => b.score - a.score);
50
+ return scored;
51
+ },
52
+ };
53
+ }
54
+ function makeSearchDocument(component) {
55
+ const parts = [
56
+ // Intent and description are the most-weighted fields. Repeat the intent
57
+ // twice so it dominates rankings when its terms match.
58
+ component.intent,
59
+ component.intent,
60
+ component.description,
61
+ component.name,
62
+ ...(component.content.variants ?? []).flatMap((v) => [v.name, v.whenToUse]),
63
+ ...component.examples.map((e) => e.intent),
64
+ ];
65
+ return parts.join(' ');
66
+ }
67
+ function tokenize(text) {
68
+ return text
69
+ .toLowerCase()
70
+ .replace(/[^\w\s]/g, ' ')
71
+ .split(/\s+/)
72
+ .filter((t) => t.length > 2 && !STOP_WORDS.has(t));
73
+ }
74
+ const STOP_WORDS = new Set([
75
+ 'the',
76
+ 'and',
77
+ 'for',
78
+ 'with',
79
+ 'that',
80
+ 'this',
81
+ 'have',
82
+ 'has',
83
+ 'had',
84
+ 'are',
85
+ 'was',
86
+ 'were',
87
+ 'been',
88
+ 'being',
89
+ 'will',
90
+ 'would',
91
+ 'should',
92
+ 'could',
93
+ 'may',
94
+ 'might',
95
+ 'must',
96
+ 'shall',
97
+ 'can',
98
+ 'these',
99
+ 'those',
100
+ 'its',
101
+ 'use',
102
+ 'used',
103
+ 'using',
104
+ 'when',
105
+ 'where',
106
+ 'how',
107
+ 'what',
108
+ 'which',
109
+ 'into',
110
+ 'onto',
111
+ 'than',
112
+ 'then',
113
+ 'them',
114
+ 'they',
115
+ 'their',
116
+ 'there',
117
+ ]);
118
+ //# sourceMappingURL=tfidf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tfidf.js","sourceRoot":"","sources":["../../src/matching/tfidf.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAAC,UAA+B;IAChE,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE;QACzC,MAAM,QAAQ,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAA;QAC9C,MAAM,MAAM,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAA;QACjC,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAA;QAC1C,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;QACrD,CAAC;QACD,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,CAAA;IAC3E,CAAC,CAAC,CAAA;IAEF,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAA;IAC7B,MAAM,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAA;IACzC,KAAK,MAAM,KAAK,IAAI,KAAK,EAAE,CAAC;QAC1B,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC;YACtC,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;QACjD,CAAC;IACH,CAAC;IACD,MAAM,GAAG,GAAG,IAAI,GAAG,EAAkB,CAAA;IACrC,KAAK,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,OAAO,EAAE,CAAC;QACjC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;IACxD,CAAC;IAED,OAAO;QACL,IAAI,CAAC,KAAa;YAChB,MAAM,WAAW,GAAG,IAAI,GAAG,EAAkB,CAAA;YAC7C,KAAK,MAAM,KAAK,IAAI,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;gBACpC,WAAW,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;YAC3D,CAAC;YAED,MAAM,MAAM,GAAsB,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;gBACpD,IAAI,KAAK,GAAG,CAAC,CAAA;gBACb,KAAK,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,IAAI,WAAW,EAAE,CAAC;oBAC7C,MAAM,eAAe,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;oBAClD,IAAI,eAAe,KAAK,CAAC;wBAAE,SAAQ;oBACnC,MAAM,EAAE,GAAG,eAAe,GAAG,KAAK,CAAC,MAAM,CAAA;oBACzC,MAAM,OAAO,GAAG,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;oBAClC,KAAK,IAAI,EAAE,GAAG,OAAO,GAAG,UAAU,CAAA;gBACpC,CAAC;gBACD,OAAO,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,KAAK,EAAE,CAAA;YAC9C,CAAC,CAAC,CAAA;YAEF,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAA;YACxC,OAAO,MAAM,CAAA;QACf,CAAC;KACF,CAAA;AACH,CAAC;AAED,SAAS,kBAAkB,CAAC,SAA4B;IACtD,MAAM,KAAK,GAAa;QACtB,yEAAyE;QACzE,uDAAuD;QACvD,SAAS,CAAC,MAAM;QAChB,SAAS,CAAC,MAAM;QAChB,SAAS,CAAC,WAAW;QACrB,SAAS,CAAC,IAAI;QACd,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC;QAC3E,GAAG,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;KAC3C,CAAA;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AACxB,CAAC;AAED,SAAS,QAAQ,CAAC,IAAY;IAC5B,OAAO,IAAI;SACR,WAAW,EAAE;SACb,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC;SACxB,KAAK,CAAC,KAAK,CAAC;SACZ,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;AACtD,CAAC;AAED,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC;IACzB,KAAK;IACL,KAAK;IACL,KAAK;IACL,MAAM;IACN,MAAM;IACN,MAAM;IACN,MAAM;IACN,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,MAAM;IACN,MAAM;IACN,OAAO;IACP,MAAM;IACN,OAAO;IACP,QAAQ;IACR,OAAO;IACP,KAAK;IACL,OAAO;IACP,MAAM;IACN,OAAO;IACP,KAAK;IACL,OAAO;IACP,OAAO;IACP,KAAK;IACL,KAAK;IACL,MAAM;IACN,OAAO;IACP,MAAM;IACN,OAAO;IACP,KAAK;IACL,MAAM;IACN,OAAO;IACP,MAAM;IACN,MAAM;IACN,MAAM;IACN,MAAM;IACN,MAAM;IACN,MAAM;IACN,OAAO;IACP,OAAO;CACR,CAAC,CAAA"}
@@ -0,0 +1,22 @@
1
+ import type { ComponentContract } from '@forumone/throughline-design-contract';
2
+ export interface RankedComponent {
3
+ component: ComponentContract;
4
+ score: number;
5
+ }
6
+ export interface Matcher {
7
+ /** Returns the indexed components ranked best-first against the query. */
8
+ rank(query: string): RankedComponent[];
9
+ }
10
+ /**
11
+ * Surface returned by {@link suggest_for_intent}. Same shape regardless of the
12
+ * underlying matcher.
13
+ */
14
+ export interface RankedSuggestion {
15
+ component: string;
16
+ score: number;
17
+ reasoning: string;
18
+ matchedIntent: string;
19
+ variant?: string;
20
+ warnings?: string[];
21
+ }
22
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/matching/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAA;AAE9E,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,iBAAiB,CAAA;IAC5B,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,OAAO;IACtB,0EAA0E;IAC1E,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,eAAe,EAAE,CAAA;CACvC;AAED;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;CACpB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/matching/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,125 @@
1
+ import { z } from 'zod';
2
+ import type { BaseCorePluginOptions } from '@forumone/throughline-plugin-contract';
3
+ import type { Manifest } from '@forumone/throughline-design-contract';
4
+ export type ManifestSource = {
5
+ type: 'object';
6
+ manifest: Manifest;
7
+ } | {
8
+ type: 'url';
9
+ url: string;
10
+ refreshInterval?: number;
11
+ } | {
12
+ type: 'payload-collection';
13
+ slug: string;
14
+ documentId?: string;
15
+ };
16
+ export interface MatchingConfig {
17
+ /**
18
+ * Strategy for ranking components against a natural-language intent.
19
+ * `tfidf` ships now and requires no external dependencies; `embeddings`
20
+ * is reserved for a follow-up.
21
+ */
22
+ strategy: 'tfidf';
23
+ /** Maximum number of recommendations from `suggest_for_intent`. Default: 5. */
24
+ maxRecommendations?: number;
25
+ }
26
+ export interface ComponentsPluginOptions extends BaseCorePluginOptions {
27
+ /** Required: where the design system manifest comes from. */
28
+ manifest: ManifestSource;
29
+ /** Optional: how the plugin matches intents to components. Defaults to TF-IDF. */
30
+ matching?: MatchingConfig;
31
+ }
32
+ export declare const ComponentsPluginOptionsSchema: z.ZodObject<{
33
+ enabled: z.ZodOptional<z.ZodBoolean>;
34
+ routePrefix: z.ZodOptional<z.ZodString>;
35
+ manifest: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
36
+ type: z.ZodLiteral<"object">;
37
+ manifest: z.ZodUnknown;
38
+ }, "strip", z.ZodTypeAny, {
39
+ type: "object";
40
+ manifest?: unknown;
41
+ }, {
42
+ type: "object";
43
+ manifest?: unknown;
44
+ }>, z.ZodObject<{
45
+ type: z.ZodLiteral<"url">;
46
+ url: z.ZodString;
47
+ refreshInterval: z.ZodOptional<z.ZodNumber>;
48
+ }, "strip", z.ZodTypeAny, {
49
+ url: string;
50
+ type: "url";
51
+ refreshInterval?: number | undefined;
52
+ }, {
53
+ url: string;
54
+ type: "url";
55
+ refreshInterval?: number | undefined;
56
+ }>, z.ZodObject<{
57
+ type: z.ZodLiteral<"payload-collection">;
58
+ slug: z.ZodString;
59
+ documentId: z.ZodOptional<z.ZodString>;
60
+ }, "strip", z.ZodTypeAny, {
61
+ type: "payload-collection";
62
+ slug: string;
63
+ documentId?: string | undefined;
64
+ }, {
65
+ type: "payload-collection";
66
+ slug: string;
67
+ documentId?: string | undefined;
68
+ }>]>;
69
+ matching: z.ZodOptional<z.ZodObject<{
70
+ strategy: z.ZodEnum<["tfidf"]>;
71
+ maxRecommendations: z.ZodOptional<z.ZodNumber>;
72
+ }, "strip", z.ZodTypeAny, {
73
+ strategy: "tfidf";
74
+ maxRecommendations?: number | undefined;
75
+ }, {
76
+ strategy: "tfidf";
77
+ maxRecommendations?: number | undefined;
78
+ }>>;
79
+ }, "strip", z.ZodTypeAny, {
80
+ manifest: {
81
+ type: "object";
82
+ manifest?: unknown;
83
+ } | {
84
+ url: string;
85
+ type: "url";
86
+ refreshInterval?: number | undefined;
87
+ } | {
88
+ type: "payload-collection";
89
+ slug: string;
90
+ documentId?: string | undefined;
91
+ };
92
+ matching?: {
93
+ strategy: "tfidf";
94
+ maxRecommendations?: number | undefined;
95
+ } | undefined;
96
+ enabled?: boolean | undefined;
97
+ routePrefix?: string | undefined;
98
+ }, {
99
+ manifest: {
100
+ type: "object";
101
+ manifest?: unknown;
102
+ } | {
103
+ url: string;
104
+ type: "url";
105
+ refreshInterval?: number | undefined;
106
+ } | {
107
+ type: "payload-collection";
108
+ slug: string;
109
+ documentId?: string | undefined;
110
+ };
111
+ matching?: {
112
+ strategy: "tfidf";
113
+ maxRecommendations?: number | undefined;
114
+ } | undefined;
115
+ enabled?: boolean | undefined;
116
+ routePrefix?: string | undefined;
117
+ }>;
118
+ /**
119
+ * Validates plugin options at load time. Throws with a multi-line message
120
+ * on schema failure or with a targeted message on cross-field violations.
121
+ * Plugins that depend on this server must initialize after `componentsPlugin`
122
+ * because the registry capability check happens during onInit.
123
+ */
124
+ export declare function validateOptions(options: ComponentsPluginOptions): ComponentsPluginOptions;
125
+ //# sourceMappingURL=options.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAA;AAClF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uCAAuC,CAAA;AAErE,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,GACtC;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAAE,GACtD;IAAE,IAAI,EAAE,oBAAoB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAErE,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,QAAQ,EAAE,OAAO,CAAA;IACjB,+EAA+E;IAC/E,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED,MAAM,WAAW,uBAAwB,SAAQ,qBAAqB;IACpE,6DAA6D;IAC7D,QAAQ,EAAE,cAAc,CAAA;IACxB,kFAAkF;IAClF,QAAQ,CAAC,EAAE,cAAc,CAAA;CAC1B;AAgBD,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAUxC,CAAA;AAEF;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,uBAAuB,GAAG,uBAAuB,CASzF"}