@fabricorg/ports 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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # @fabricorg/ports
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - c504d0a: First release. Supplies the interface behind a declared port requirement, which previously resolved
8
+ against nothing.
9
+
10
+ `FlagsPort` is structurally compatible with an OpenFeature provider's evaluation surface;
11
+ `DesignTokensPort` consumes W3C DTCG documents, and `resolveDesignTokens` flattens a document,
12
+ follows aliases, inherits `$type` from the nearest declaring ancestor, and refuses both a dangling
13
+ alias and an alias cycle. Nothing in the package imports a vendor SDK.
14
+
15
+ `validatePortRequirements` checks that every port a capability declares is backed by a registered
16
+ adapter speaking the required standard, and `assertPortRequirementsSatisfied` turns that into a
17
+ deployment gate naming every unsatisfied port. `flagsPortChecks` and `designTokensPortChecks` are the
18
+ suite every adapter must pass, run by `runPortContract`, which reports every failure rather than
19
+ stopping at the first.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fabric
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 ADDED
@@ -0,0 +1,41 @@
1
+ # @fabricorg/ports
2
+
3
+ Vendor-neutral port interfaces, a W3C DTCG token resolver, and the contract-test kit every adapter must pass.
4
+
5
+ A capability declares that it needs a port — `CapabilityPortRequirement` in its manifest. Until now that was a name resolving against nothing. This package supplies the interface behind the name, checks that a registered adapter actually satisfies the declaration, and gives every adapter the same suite to pass.
6
+
7
+ ```bash
8
+ pnpm add @fabricorg/ports
9
+ ```
10
+
11
+ ## Where a standard exists, the port speaks it
12
+
13
+ `FlagsPort` is structurally compatible with an OpenFeature provider's evaluation surface, so an OpenFeature provider is a thin adapter rather than a translation layer. `DesignTokensPort` consumes W3C DTCG documents, so a design tool is one exporter among any.
14
+
15
+ Nothing here imports a vendor SDK. A port is a shape; an adapter is anything that satisfies it.
16
+
17
+ ## Turning a declaration into a gate
18
+
19
+ ```ts
20
+ import { assertPortRequirementsSatisfied } from "@fabricorg/ports";
21
+
22
+ assertPortRequirementsSatisfied({ capability, adapters });
23
+ ```
24
+
25
+ Fails when a declared port has no registered adapter, when no adapter speaks a required standard, or when none offers a required version — with every unsatisfied port named. Version matching is exact; range negotiation belongs to whatever installs the adapters, and half-implemented semver would be worse than none.
26
+
27
+ ## The adapter contract
28
+
29
+ ```ts
30
+ import { flagsPortChecks, runPortContract } from "@fabricorg/ports";
31
+
32
+ const { passed, failures } = await runPortContract(adapter, flagsPortChecks());
33
+ ```
34
+
35
+ Every flags adapter must default rather than throw on an unknown key, return each resolver's own type, evaluate repeatably, and tolerate an absent context. Every design-token adapter must resolve a theme to at least one token, follow every alias, and keep names unique. `runPortContract` reports every failure rather than stopping at the first, so one run tells an adapter author everything to fix.
36
+
37
+ That is what makes swapping a vendor an adapter build plus a configuration change rather than a migration.
38
+
39
+ ## DTCG resolution
40
+
41
+ `resolveDesignTokens` flattens a DTCG document to dotted names, follows `{alias}` references to their concrete value, inherits `$type` from the nearest ancestor group that declares one, and refuses both a dangling alias and an alias cycle.
package/dist/index.cjs ADDED
@@ -0,0 +1,226 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ assertPortRequirementsSatisfied: () => assertPortRequirementsSatisfied,
24
+ designTokensPortChecks: () => designTokensPortChecks,
25
+ flagsPortChecks: () => flagsPortChecks,
26
+ resolveDesignTokens: () => resolveDesignTokens,
27
+ runPortContract: () => runPortContract,
28
+ validatePortRequirements: () => validatePortRequirements
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+ var ALIAS = /^\{([^}]+)\}$/;
32
+ var isToken = (value) => !!value && typeof value === "object" && !Array.isArray(value) && "$value" in value;
33
+ var isGroup = (value) => !!value && typeof value === "object" && !Array.isArray(value) && !("$value" in value);
34
+ function resolveDesignTokens(document) {
35
+ const flat = /* @__PURE__ */ new Map();
36
+ const walk = (node, path, inheritedType) => {
37
+ const groupType = typeof node.$type === "string" ? node.$type : inheritedType;
38
+ for (const [key, member] of Object.entries(node)) {
39
+ if (key.startsWith("$")) continue;
40
+ const next = [...path, key];
41
+ if (isToken(member)) {
42
+ flat.set(next.join("."), { token: member, type: member.$type ?? groupType });
43
+ } else if (isGroup(member)) {
44
+ walk(member, next, groupType);
45
+ }
46
+ }
47
+ };
48
+ walk(document, []);
49
+ const resolving = /* @__PURE__ */ new Set();
50
+ const resolved = /* @__PURE__ */ new Map();
51
+ const valueOf = (name) => {
52
+ if (resolved.has(name)) return resolved.get(name);
53
+ const entry = flat.get(name);
54
+ if (!entry) throw new Error(`Design token alias "{${name}}" does not resolve to a declared token.`);
55
+ if (resolving.has(name)) {
56
+ throw new Error(`Design token alias cycle: ${[...resolving, name].join(" -> ")}.`);
57
+ }
58
+ const raw = entry.token.$value;
59
+ if (typeof raw !== "string") {
60
+ resolved.set(name, raw);
61
+ return raw;
62
+ }
63
+ const alias = ALIAS.exec(raw.trim());
64
+ if (!alias) {
65
+ resolved.set(name, raw);
66
+ return raw;
67
+ }
68
+ resolving.add(name);
69
+ const target = valueOf(alias[1]);
70
+ resolving.delete(name);
71
+ resolved.set(name, target);
72
+ return target;
73
+ };
74
+ return [...flat.entries()].map(([name, entry]) => {
75
+ const value = valueOf(name);
76
+ return {
77
+ name,
78
+ value,
79
+ ...entry.type === void 0 ? {} : { type: entry.type },
80
+ ...entry.token.$description === void 0 ? {} : { description: entry.token.$description }
81
+ };
82
+ });
83
+ }
84
+ function validatePortRequirements(input) {
85
+ const findings = [];
86
+ for (const requirement of input.capability.requirements?.ports ?? []) {
87
+ const candidates = input.adapters.filter((adapter) => adapter.port === requirement.name);
88
+ if (candidates.length === 0) {
89
+ findings.push({
90
+ code: "unsatisfied_port",
91
+ port: requirement.name,
92
+ message: `capability "${input.capability.namespace}" requires port "${requirement.name}" and no adapter is registered for it`
93
+ });
94
+ continue;
95
+ }
96
+ if (requirement.standard) {
97
+ const speaking = candidates.filter((adapter) => adapter.standard?.name === requirement.standard.name);
98
+ if (speaking.length === 0) {
99
+ findings.push({
100
+ code: "standard_not_spoken",
101
+ port: requirement.name,
102
+ message: `port "${requirement.name}" must speak "${requirement.standard.name}"; registered adapters are ${candidates.map((adapter) => `${adapter.vendor}(${adapter.standard?.name ?? "no standard"})`).join(", ")}`
103
+ });
104
+ continue;
105
+ }
106
+ }
107
+ if (requirement.version && !candidates.some((adapter) => adapter.version === requirement.version)) {
108
+ findings.push({
109
+ code: "version_mismatch",
110
+ port: requirement.name,
111
+ message: `port "${requirement.name}" requires version "${requirement.version}"; registered adapters are ${candidates.map((adapter) => `${adapter.vendor}@${adapter.version ?? "unversioned"}`).join(", ")}`
112
+ });
113
+ }
114
+ }
115
+ return { valid: findings.length === 0, findings };
116
+ }
117
+ function assertPortRequirementsSatisfied(input) {
118
+ const result = validatePortRequirements(input);
119
+ if (result.valid) return;
120
+ throw new Error([
121
+ `Port requirements for "${input.capability.namespace}" are not satisfied:`,
122
+ ...result.findings.map((finding) => ` - ${finding.message}`)
123
+ ].join("\n"));
124
+ }
125
+ var PortContractFailure = class extends Error {
126
+ name = "PortContractFailure";
127
+ };
128
+ function expect(condition, message) {
129
+ if (!condition) throw new PortContractFailure(message);
130
+ }
131
+ function flagsPortChecks() {
132
+ const unknownKey = "fabric.contract.definitely-not-configured";
133
+ return [
134
+ {
135
+ id: "flags.default-on-unknown-key",
136
+ title: "an unknown flag resolves to the supplied default rather than throwing",
137
+ async run(port) {
138
+ const result = await port.resolveBoolean(unknownKey, true);
139
+ expect(result.value === true, `an unknown flag returned ${String(result.value)} instead of the supplied default`);
140
+ expect(
141
+ result.reason === "DEFAULT" || result.reason === "ERROR",
142
+ `an unknown flag resolved with reason "${result.reason}"; a default or error was expected`
143
+ );
144
+ }
145
+ },
146
+ {
147
+ id: "flags.default-is-typed",
148
+ title: "each typed resolver returns its own type",
149
+ async run(port) {
150
+ expect(typeof (await port.resolveBoolean(unknownKey, false)).value === "boolean", "resolveBoolean did not return a boolean");
151
+ expect(typeof (await port.resolveString(unknownKey, "fallback")).value === "string", "resolveString did not return a string");
152
+ expect(typeof (await port.resolveNumber(unknownKey, 42)).value === "number", "resolveNumber did not return a number");
153
+ }
154
+ },
155
+ {
156
+ id: "flags.evaluation-is-pure",
157
+ title: "the same key and context resolve the same way twice",
158
+ async run(port) {
159
+ const context = { targetingKey: "fabric-contract-subject" };
160
+ const first = await port.resolveString(unknownKey, "fallback", context);
161
+ const second = await port.resolveString(unknownKey, "fallback", context);
162
+ expect(first.value === second.value, `the same evaluation returned "${first.value}" then "${second.value}"`);
163
+ }
164
+ },
165
+ {
166
+ id: "flags.tolerates-absent-context",
167
+ title: "evaluation without a context does not throw",
168
+ async run(port) {
169
+ await port.resolveBoolean(unknownKey, false);
170
+ }
171
+ }
172
+ ];
173
+ }
174
+ function designTokensPortChecks(theme) {
175
+ return [
176
+ {
177
+ id: "tokens.theme-resolves",
178
+ title: "a known theme resolves to at least one token",
179
+ async run(port) {
180
+ const tokens = await port.resolve(theme);
181
+ expect(Array.isArray(tokens) && tokens.length > 0, `theme "${theme}" resolved to no tokens`);
182
+ }
183
+ },
184
+ {
185
+ id: "tokens.no-unresolved-aliases",
186
+ title: "no resolved token still carries an alias",
187
+ async run(port) {
188
+ for (const token of await port.resolve(theme)) {
189
+ expect(
190
+ typeof token.value !== "string" || !ALIAS.test(token.value.trim()),
191
+ `token "${token.name}" resolved to the unfollowed alias ${String(token.value)}`
192
+ );
193
+ }
194
+ }
195
+ },
196
+ {
197
+ id: "tokens.names-are-unique",
198
+ title: "token names are unique within a theme",
199
+ async run(port) {
200
+ const names = (await port.resolve(theme)).map((token) => token.name);
201
+ expect(new Set(names).size === names.length, "the theme resolved duplicate token names");
202
+ }
203
+ }
204
+ ];
205
+ }
206
+ async function runPortContract(port, checks) {
207
+ const failures = [];
208
+ for (const check of checks) {
209
+ try {
210
+ await check.run(port);
211
+ } catch (error) {
212
+ failures.push({ id: check.id, message: error instanceof Error ? error.message : String(error) });
213
+ }
214
+ }
215
+ return { passed: failures.length === 0, failures };
216
+ }
217
+ // Annotate the CommonJS export names for ESM import in node:
218
+ 0 && (module.exports = {
219
+ assertPortRequirementsSatisfied,
220
+ designTokensPortChecks,
221
+ flagsPortChecks,
222
+ resolveDesignTokens,
223
+ runPortContract,
224
+ validatePortRequirements
225
+ });
226
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../index.ts"],"sourcesContent":["/**\n * Port interfaces are declared here and implemented by vendor adapters elsewhere.\n * Nothing in this package imports a vendor SDK: a port is a shape, and an adapter\n * is anything that satisfies it. Where an open standard exists the port speaks it,\n * so the incumbent vendor is already just one provider behind the interface.\n */\n\n// ── Feature flags (OpenFeature-shaped) ──────────────────────────────────────\n\nexport type FlagValue = boolean | string | number | { [key: string]: unknown };\n\nexport interface EvaluationContext {\n\t/** Stable identifier for the subject of evaluation, usually a tenant or actor. */\n\ttargetingKey?: string;\n\t[attribute: string]: unknown;\n}\n\nexport type ResolutionReason = \"STATIC\" | \"DEFAULT\" | \"TARGETING_MATCH\" | \"SPLIT\" | \"CACHED\" | \"ERROR\";\n\nexport interface ResolutionDetails<T extends FlagValue> {\n\tvalue: T;\n\treason: ResolutionReason;\n\tvariant?: string;\n\terrorCode?: string;\n}\n\n/**\n * Structurally compatible with an OpenFeature provider's evaluation surface, so an\n * OpenFeature provider is a thin adapter rather than a translation layer.\n */\nexport interface FlagsPort {\n\tresolveBoolean(flagKey: string, defaultValue: boolean, context?: EvaluationContext): Promise<ResolutionDetails<boolean>>;\n\tresolveString(flagKey: string, defaultValue: string, context?: EvaluationContext): Promise<ResolutionDetails<string>>;\n\tresolveNumber(flagKey: string, defaultValue: number, context?: EvaluationContext): Promise<ResolutionDetails<number>>;\n}\n\n// ── Design tokens (W3C DTCG) ────────────────────────────────────────────────\n\nexport interface DtcgToken {\n\t$value: unknown;\n\t$type?: string;\n\t$description?: string;\n}\n\nexport interface DtcgGroup {\n\t$type?: string;\n\t$description?: string;\n\t[member: string]: DtcgToken | DtcgGroup | string | undefined;\n}\n\nexport interface ResolvedToken {\n\tname: string;\n\tvalue: unknown;\n\ttype?: string;\n\tdescription?: string;\n}\n\nexport interface DesignTokensPort {\n\t/** Resolved tokens for one theme. Aliases are already followed. */\n\tresolve(theme: string): Promise<ResolvedToken[]>;\n}\n\nconst ALIAS = /^\\{([^}]+)\\}$/;\n\nconst isToken = (value: unknown): value is DtcgToken =>\n\t!!value && typeof value === \"object\" && !Array.isArray(value) && \"$value\" in value;\n\nconst isGroup = (value: unknown): value is DtcgGroup =>\n\t!!value && typeof value === \"object\" && !Array.isArray(value) && !(\"$value\" in value);\n\n/**\n * Flattens a DTCG document and follows aliases. `$type` is inherited from the\n * nearest ancestor group that declares one, which is what the format specifies\n * and what makes a theme file readable.\n */\nexport function resolveDesignTokens(document: DtcgGroup): ResolvedToken[] {\n\tconst flat = new Map<string, { token: DtcgToken; type?: string }>();\n\n\tconst walk = (node: DtcgGroup, path: string[], inheritedType?: string): void => {\n\t\tconst groupType = typeof node.$type === \"string\" ? node.$type : inheritedType;\n\t\tfor (const [key, member] of Object.entries(node)) {\n\t\t\tif (key.startsWith(\"$\")) continue;\n\t\t\tconst next = [...path, key];\n\t\t\tif (isToken(member)) {\n\t\t\t\tflat.set(next.join(\".\"), { token: member, type: member.$type ?? groupType });\n\t\t\t} else if (isGroup(member)) {\n\t\t\t\twalk(member, next, groupType);\n\t\t\t}\n\t\t}\n\t};\n\twalk(document, []);\n\n\tconst resolving = new Set<string>();\n\tconst resolved = new Map<string, unknown>();\n\n\tconst valueOf = (name: string): unknown => {\n\t\tif (resolved.has(name)) return resolved.get(name);\n\t\tconst entry = flat.get(name);\n\t\tif (!entry) throw new Error(`Design token alias \"{${name}}\" does not resolve to a declared token.`);\n\t\tif (resolving.has(name)) {\n\t\t\tthrow new Error(`Design token alias cycle: ${[...resolving, name].join(\" -> \")}.`);\n\t\t}\n\t\tconst raw = entry.token.$value;\n\t\tif (typeof raw !== \"string\") {\n\t\t\tresolved.set(name, raw);\n\t\t\treturn raw;\n\t\t}\n\t\tconst alias = ALIAS.exec(raw.trim());\n\t\tif (!alias) {\n\t\t\tresolved.set(name, raw);\n\t\t\treturn raw;\n\t\t}\n\t\tresolving.add(name);\n\t\tconst target = valueOf(alias[1]!);\n\t\tresolving.delete(name);\n\t\tresolved.set(name, target);\n\t\treturn target;\n\t};\n\n\treturn [...flat.entries()].map(([name, entry]) => {\n\t\tconst value = valueOf(name);\n\t\treturn {\n\t\t\tname,\n\t\t\tvalue,\n\t\t\t...(entry.type === undefined ? {} : { type: entry.type }),\n\t\t\t...(entry.token.$description === undefined ? {} : { description: entry.token.$description }),\n\t\t};\n\t});\n}\n\n// ── Requirement satisfaction ────────────────────────────────────────────────\n\nexport interface PortRequirement {\n\tname: string;\n\tversion?: string;\n\tstandard?: { name: string; version?: string };\n}\n\n/** The subset of capability metadata that declares what ports it needs. */\nexport interface PortRequiringCapability {\n\tnamespace: string;\n\trequirements?: { ports?: PortRequirement[] };\n}\n\nexport interface RegisteredAdapter {\n\t/** Port this adapter implements, matching the requirement's `name`. */\n\tport: string;\n\tvendor: string;\n\tversion?: string;\n\tstandard?: { name: string; version?: string };\n}\n\nexport type PortFindingCode = \"unsatisfied_port\" | \"standard_not_spoken\" | \"version_mismatch\";\n\nexport interface PortFinding {\n\tcode: PortFindingCode;\n\tport: string;\n\tmessage: string;\n}\n\nexport interface PortValidationResult {\n\tvalid: boolean;\n\tfindings: PortFinding[];\n}\n\n/**\n * Checks that every port a capability declares is backed by a registered adapter.\n * `CapabilityPortRequirement` is otherwise a name that resolves against nothing;\n * this is what turns the declaration into a deployment-time gate.\n *\n * Version matching is exact. Range negotiation belongs to whatever installs the\n * adapters, and pretending to do semver here would be worse than not doing it.\n */\nexport function validatePortRequirements(input: {\n\tcapability: PortRequiringCapability;\n\tadapters: readonly RegisteredAdapter[];\n}): PortValidationResult {\n\tconst findings: PortFinding[] = [];\n\tfor (const requirement of input.capability.requirements?.ports ?? []) {\n\t\tconst candidates = input.adapters.filter((adapter) => adapter.port === requirement.name);\n\t\tif (candidates.length === 0) {\n\t\t\tfindings.push({\n\t\t\t\tcode: \"unsatisfied_port\",\n\t\t\t\tport: requirement.name,\n\t\t\t\tmessage: `capability \"${input.capability.namespace}\" requires port \"${requirement.name}\" and no adapter is registered for it`,\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tif (requirement.standard) {\n\t\t\tconst speaking = candidates.filter((adapter) => adapter.standard?.name === requirement.standard!.name);\n\t\t\tif (speaking.length === 0) {\n\t\t\t\tfindings.push({\n\t\t\t\t\tcode: \"standard_not_spoken\",\n\t\t\t\t\tport: requirement.name,\n\t\t\t\t\tmessage: `port \"${requirement.name}\" must speak \"${requirement.standard.name}\"; registered adapters are ${candidates.map((adapter) => `${adapter.vendor}(${adapter.standard?.name ?? \"no standard\"})`).join(\", \")}`,\n\t\t\t\t});\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tif (requirement.version && !candidates.some((adapter) => adapter.version === requirement.version)) {\n\t\t\tfindings.push({\n\t\t\t\tcode: \"version_mismatch\",\n\t\t\t\tport: requirement.name,\n\t\t\t\tmessage: `port \"${requirement.name}\" requires version \"${requirement.version}\"; registered adapters are ${candidates.map((adapter) => `${adapter.vendor}@${adapter.version ?? \"unversioned\"}`).join(\", \")}`,\n\t\t\t});\n\t\t}\n\t}\n\treturn { valid: findings.length === 0, findings };\n}\n\nexport function assertPortRequirementsSatisfied(input: {\n\tcapability: PortRequiringCapability;\n\tadapters: readonly RegisteredAdapter[];\n}): void {\n\tconst result = validatePortRequirements(input);\n\tif (result.valid) return;\n\tthrow new Error([\n\t\t`Port requirements for \"${input.capability.namespace}\" are not satisfied:`,\n\t\t...result.findings.map((finding) => ` - ${finding.message}`),\n\t].join(\"\\n\"));\n}\n\n// ── Adapter contract kit ────────────────────────────────────────────────────\n\nexport interface PortCheck<TPort> {\n\tid: string;\n\ttitle: string;\n\t/** Throws on failure, exactly as an assertion would. */\n\trun(port: TPort): Promise<void>;\n}\n\nclass PortContractFailure extends Error {\n\toverride readonly name = \"PortContractFailure\";\n}\n\nfunction expect(condition: unknown, message: string): asserts condition {\n\tif (!condition) throw new PortContractFailure(message);\n}\n\n/**\n * Every flags adapter must pass these, so swapping vendors is an adapter build\n * plus a configuration change rather than a migration.\n */\nexport function flagsPortChecks(): PortCheck<FlagsPort>[] {\n\tconst unknownKey = \"fabric.contract.definitely-not-configured\";\n\treturn [\n\t\t{\n\t\t\tid: \"flags.default-on-unknown-key\",\n\t\t\ttitle: \"an unknown flag resolves to the supplied default rather than throwing\",\n\t\t\tasync run(port) {\n\t\t\t\tconst result = await port.resolveBoolean(unknownKey, true);\n\t\t\t\texpect(result.value === true, `an unknown flag returned ${String(result.value)} instead of the supplied default`);\n\t\t\t\texpect(\n\t\t\t\t\tresult.reason === \"DEFAULT\" || result.reason === \"ERROR\",\n\t\t\t\t\t`an unknown flag resolved with reason \"${result.reason}\"; a default or error was expected`,\n\t\t\t\t);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"flags.default-is-typed\",\n\t\t\ttitle: \"each typed resolver returns its own type\",\n\t\t\tasync run(port) {\n\t\t\t\texpect(typeof (await port.resolveBoolean(unknownKey, false)).value === \"boolean\", \"resolveBoolean did not return a boolean\");\n\t\t\t\texpect(typeof (await port.resolveString(unknownKey, \"fallback\")).value === \"string\", \"resolveString did not return a string\");\n\t\t\t\texpect(typeof (await port.resolveNumber(unknownKey, 42)).value === \"number\", \"resolveNumber did not return a number\");\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"flags.evaluation-is-pure\",\n\t\t\ttitle: \"the same key and context resolve the same way twice\",\n\t\t\tasync run(port) {\n\t\t\t\tconst context = { targetingKey: \"fabric-contract-subject\" };\n\t\t\t\tconst first = await port.resolveString(unknownKey, \"fallback\", context);\n\t\t\t\tconst second = await port.resolveString(unknownKey, \"fallback\", context);\n\t\t\t\texpect(first.value === second.value, `the same evaluation returned \"${first.value}\" then \"${second.value}\"`);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"flags.tolerates-absent-context\",\n\t\t\ttitle: \"evaluation without a context does not throw\",\n\t\t\tasync run(port) {\n\t\t\t\tawait port.resolveBoolean(unknownKey, false);\n\t\t\t},\n\t\t},\n\t];\n}\n\nexport function designTokensPortChecks(theme: string): PortCheck<DesignTokensPort>[] {\n\treturn [\n\t\t{\n\t\t\tid: \"tokens.theme-resolves\",\n\t\t\ttitle: \"a known theme resolves to at least one token\",\n\t\t\tasync run(port) {\n\t\t\t\tconst tokens = await port.resolve(theme);\n\t\t\t\texpect(Array.isArray(tokens) && tokens.length > 0, `theme \"${theme}\" resolved to no tokens`);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"tokens.no-unresolved-aliases\",\n\t\t\ttitle: \"no resolved token still carries an alias\",\n\t\t\tasync run(port) {\n\t\t\t\tfor (const token of await port.resolve(theme)) {\n\t\t\t\t\texpect(\n\t\t\t\t\t\ttypeof token.value !== \"string\" || !ALIAS.test(token.value.trim()),\n\t\t\t\t\t\t`token \"${token.name}\" resolved to the unfollowed alias ${String(token.value)}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"tokens.names-are-unique\",\n\t\t\ttitle: \"token names are unique within a theme\",\n\t\t\tasync run(port) {\n\t\t\t\tconst names = (await port.resolve(theme)).map((token) => token.name);\n\t\t\t\texpect(new Set(names).size === names.length, \"the theme resolved duplicate token names\");\n\t\t\t},\n\t\t},\n\t];\n}\n\n/** Runs a contract suite and returns every failure, rather than stopping at the first. */\nexport async function runPortContract<TPort>(port: TPort, checks: readonly PortCheck<TPort>[]): Promise<{ passed: boolean; failures: Array<{ id: string; message: string }> }> {\n\tconst failures: Array<{ id: string; message: string }> = [];\n\tfor (const check of checks) {\n\t\ttry {\n\t\t\tawait check.run(port);\n\t\t} catch (error) {\n\t\t\tfailures.push({ id: check.id, message: error instanceof Error ? error.message : String(error) });\n\t\t}\n\t}\n\treturn { passed: failures.length === 0, failures };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Port interfaces are declared here and implemented by vendor adapters elsewhere.
3
+ * Nothing in this package imports a vendor SDK: a port is a shape, and an adapter
4
+ * is anything that satisfies it. Where an open standard exists the port speaks it,
5
+ * so the incumbent vendor is already just one provider behind the interface.
6
+ */
7
+ type FlagValue = boolean | string | number | {
8
+ [key: string]: unknown;
9
+ };
10
+ interface EvaluationContext {
11
+ /** Stable identifier for the subject of evaluation, usually a tenant or actor. */
12
+ targetingKey?: string;
13
+ [attribute: string]: unknown;
14
+ }
15
+ type ResolutionReason = "STATIC" | "DEFAULT" | "TARGETING_MATCH" | "SPLIT" | "CACHED" | "ERROR";
16
+ interface ResolutionDetails<T extends FlagValue> {
17
+ value: T;
18
+ reason: ResolutionReason;
19
+ variant?: string;
20
+ errorCode?: string;
21
+ }
22
+ /**
23
+ * Structurally compatible with an OpenFeature provider's evaluation surface, so an
24
+ * OpenFeature provider is a thin adapter rather than a translation layer.
25
+ */
26
+ interface FlagsPort {
27
+ resolveBoolean(flagKey: string, defaultValue: boolean, context?: EvaluationContext): Promise<ResolutionDetails<boolean>>;
28
+ resolveString(flagKey: string, defaultValue: string, context?: EvaluationContext): Promise<ResolutionDetails<string>>;
29
+ resolveNumber(flagKey: string, defaultValue: number, context?: EvaluationContext): Promise<ResolutionDetails<number>>;
30
+ }
31
+ interface DtcgToken {
32
+ $value: unknown;
33
+ $type?: string;
34
+ $description?: string;
35
+ }
36
+ interface DtcgGroup {
37
+ $type?: string;
38
+ $description?: string;
39
+ [member: string]: DtcgToken | DtcgGroup | string | undefined;
40
+ }
41
+ interface ResolvedToken {
42
+ name: string;
43
+ value: unknown;
44
+ type?: string;
45
+ description?: string;
46
+ }
47
+ interface DesignTokensPort {
48
+ /** Resolved tokens for one theme. Aliases are already followed. */
49
+ resolve(theme: string): Promise<ResolvedToken[]>;
50
+ }
51
+ /**
52
+ * Flattens a DTCG document and follows aliases. `$type` is inherited from the
53
+ * nearest ancestor group that declares one, which is what the format specifies
54
+ * and what makes a theme file readable.
55
+ */
56
+ declare function resolveDesignTokens(document: DtcgGroup): ResolvedToken[];
57
+ interface PortRequirement {
58
+ name: string;
59
+ version?: string;
60
+ standard?: {
61
+ name: string;
62
+ version?: string;
63
+ };
64
+ }
65
+ /** The subset of capability metadata that declares what ports it needs. */
66
+ interface PortRequiringCapability {
67
+ namespace: string;
68
+ requirements?: {
69
+ ports?: PortRequirement[];
70
+ };
71
+ }
72
+ interface RegisteredAdapter {
73
+ /** Port this adapter implements, matching the requirement's `name`. */
74
+ port: string;
75
+ vendor: string;
76
+ version?: string;
77
+ standard?: {
78
+ name: string;
79
+ version?: string;
80
+ };
81
+ }
82
+ type PortFindingCode = "unsatisfied_port" | "standard_not_spoken" | "version_mismatch";
83
+ interface PortFinding {
84
+ code: PortFindingCode;
85
+ port: string;
86
+ message: string;
87
+ }
88
+ interface PortValidationResult {
89
+ valid: boolean;
90
+ findings: PortFinding[];
91
+ }
92
+ /**
93
+ * Checks that every port a capability declares is backed by a registered adapter.
94
+ * `CapabilityPortRequirement` is otherwise a name that resolves against nothing;
95
+ * this is what turns the declaration into a deployment-time gate.
96
+ *
97
+ * Version matching is exact. Range negotiation belongs to whatever installs the
98
+ * adapters, and pretending to do semver here would be worse than not doing it.
99
+ */
100
+ declare function validatePortRequirements(input: {
101
+ capability: PortRequiringCapability;
102
+ adapters: readonly RegisteredAdapter[];
103
+ }): PortValidationResult;
104
+ declare function assertPortRequirementsSatisfied(input: {
105
+ capability: PortRequiringCapability;
106
+ adapters: readonly RegisteredAdapter[];
107
+ }): void;
108
+ interface PortCheck<TPort> {
109
+ id: string;
110
+ title: string;
111
+ /** Throws on failure, exactly as an assertion would. */
112
+ run(port: TPort): Promise<void>;
113
+ }
114
+ /**
115
+ * Every flags adapter must pass these, so swapping vendors is an adapter build
116
+ * plus a configuration change rather than a migration.
117
+ */
118
+ declare function flagsPortChecks(): PortCheck<FlagsPort>[];
119
+ declare function designTokensPortChecks(theme: string): PortCheck<DesignTokensPort>[];
120
+ /** Runs a contract suite and returns every failure, rather than stopping at the first. */
121
+ declare function runPortContract<TPort>(port: TPort, checks: readonly PortCheck<TPort>[]): Promise<{
122
+ passed: boolean;
123
+ failures: Array<{
124
+ id: string;
125
+ message: string;
126
+ }>;
127
+ }>;
128
+
129
+ export { type DesignTokensPort, type DtcgGroup, type DtcgToken, type EvaluationContext, type FlagValue, type FlagsPort, type PortCheck, type PortFinding, type PortFindingCode, type PortRequirement, type PortRequiringCapability, type PortValidationResult, type RegisteredAdapter, type ResolutionDetails, type ResolutionReason, type ResolvedToken, assertPortRequirementsSatisfied, designTokensPortChecks, flagsPortChecks, resolveDesignTokens, runPortContract, validatePortRequirements };
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Port interfaces are declared here and implemented by vendor adapters elsewhere.
3
+ * Nothing in this package imports a vendor SDK: a port is a shape, and an adapter
4
+ * is anything that satisfies it. Where an open standard exists the port speaks it,
5
+ * so the incumbent vendor is already just one provider behind the interface.
6
+ */
7
+ type FlagValue = boolean | string | number | {
8
+ [key: string]: unknown;
9
+ };
10
+ interface EvaluationContext {
11
+ /** Stable identifier for the subject of evaluation, usually a tenant or actor. */
12
+ targetingKey?: string;
13
+ [attribute: string]: unknown;
14
+ }
15
+ type ResolutionReason = "STATIC" | "DEFAULT" | "TARGETING_MATCH" | "SPLIT" | "CACHED" | "ERROR";
16
+ interface ResolutionDetails<T extends FlagValue> {
17
+ value: T;
18
+ reason: ResolutionReason;
19
+ variant?: string;
20
+ errorCode?: string;
21
+ }
22
+ /**
23
+ * Structurally compatible with an OpenFeature provider's evaluation surface, so an
24
+ * OpenFeature provider is a thin adapter rather than a translation layer.
25
+ */
26
+ interface FlagsPort {
27
+ resolveBoolean(flagKey: string, defaultValue: boolean, context?: EvaluationContext): Promise<ResolutionDetails<boolean>>;
28
+ resolveString(flagKey: string, defaultValue: string, context?: EvaluationContext): Promise<ResolutionDetails<string>>;
29
+ resolveNumber(flagKey: string, defaultValue: number, context?: EvaluationContext): Promise<ResolutionDetails<number>>;
30
+ }
31
+ interface DtcgToken {
32
+ $value: unknown;
33
+ $type?: string;
34
+ $description?: string;
35
+ }
36
+ interface DtcgGroup {
37
+ $type?: string;
38
+ $description?: string;
39
+ [member: string]: DtcgToken | DtcgGroup | string | undefined;
40
+ }
41
+ interface ResolvedToken {
42
+ name: string;
43
+ value: unknown;
44
+ type?: string;
45
+ description?: string;
46
+ }
47
+ interface DesignTokensPort {
48
+ /** Resolved tokens for one theme. Aliases are already followed. */
49
+ resolve(theme: string): Promise<ResolvedToken[]>;
50
+ }
51
+ /**
52
+ * Flattens a DTCG document and follows aliases. `$type` is inherited from the
53
+ * nearest ancestor group that declares one, which is what the format specifies
54
+ * and what makes a theme file readable.
55
+ */
56
+ declare function resolveDesignTokens(document: DtcgGroup): ResolvedToken[];
57
+ interface PortRequirement {
58
+ name: string;
59
+ version?: string;
60
+ standard?: {
61
+ name: string;
62
+ version?: string;
63
+ };
64
+ }
65
+ /** The subset of capability metadata that declares what ports it needs. */
66
+ interface PortRequiringCapability {
67
+ namespace: string;
68
+ requirements?: {
69
+ ports?: PortRequirement[];
70
+ };
71
+ }
72
+ interface RegisteredAdapter {
73
+ /** Port this adapter implements, matching the requirement's `name`. */
74
+ port: string;
75
+ vendor: string;
76
+ version?: string;
77
+ standard?: {
78
+ name: string;
79
+ version?: string;
80
+ };
81
+ }
82
+ type PortFindingCode = "unsatisfied_port" | "standard_not_spoken" | "version_mismatch";
83
+ interface PortFinding {
84
+ code: PortFindingCode;
85
+ port: string;
86
+ message: string;
87
+ }
88
+ interface PortValidationResult {
89
+ valid: boolean;
90
+ findings: PortFinding[];
91
+ }
92
+ /**
93
+ * Checks that every port a capability declares is backed by a registered adapter.
94
+ * `CapabilityPortRequirement` is otherwise a name that resolves against nothing;
95
+ * this is what turns the declaration into a deployment-time gate.
96
+ *
97
+ * Version matching is exact. Range negotiation belongs to whatever installs the
98
+ * adapters, and pretending to do semver here would be worse than not doing it.
99
+ */
100
+ declare function validatePortRequirements(input: {
101
+ capability: PortRequiringCapability;
102
+ adapters: readonly RegisteredAdapter[];
103
+ }): PortValidationResult;
104
+ declare function assertPortRequirementsSatisfied(input: {
105
+ capability: PortRequiringCapability;
106
+ adapters: readonly RegisteredAdapter[];
107
+ }): void;
108
+ interface PortCheck<TPort> {
109
+ id: string;
110
+ title: string;
111
+ /** Throws on failure, exactly as an assertion would. */
112
+ run(port: TPort): Promise<void>;
113
+ }
114
+ /**
115
+ * Every flags adapter must pass these, so swapping vendors is an adapter build
116
+ * plus a configuration change rather than a migration.
117
+ */
118
+ declare function flagsPortChecks(): PortCheck<FlagsPort>[];
119
+ declare function designTokensPortChecks(theme: string): PortCheck<DesignTokensPort>[];
120
+ /** Runs a contract suite and returns every failure, rather than stopping at the first. */
121
+ declare function runPortContract<TPort>(port: TPort, checks: readonly PortCheck<TPort>[]): Promise<{
122
+ passed: boolean;
123
+ failures: Array<{
124
+ id: string;
125
+ message: string;
126
+ }>;
127
+ }>;
128
+
129
+ export { type DesignTokensPort, type DtcgGroup, type DtcgToken, type EvaluationContext, type FlagValue, type FlagsPort, type PortCheck, type PortFinding, type PortFindingCode, type PortRequirement, type PortRequiringCapability, type PortValidationResult, type RegisteredAdapter, type ResolutionDetails, type ResolutionReason, type ResolvedToken, assertPortRequirementsSatisfied, designTokensPortChecks, flagsPortChecks, resolveDesignTokens, runPortContract, validatePortRequirements };
package/dist/index.js ADDED
@@ -0,0 +1,196 @@
1
+ // index.ts
2
+ var ALIAS = /^\{([^}]+)\}$/;
3
+ var isToken = (value) => !!value && typeof value === "object" && !Array.isArray(value) && "$value" in value;
4
+ var isGroup = (value) => !!value && typeof value === "object" && !Array.isArray(value) && !("$value" in value);
5
+ function resolveDesignTokens(document) {
6
+ const flat = /* @__PURE__ */ new Map();
7
+ const walk = (node, path, inheritedType) => {
8
+ const groupType = typeof node.$type === "string" ? node.$type : inheritedType;
9
+ for (const [key, member] of Object.entries(node)) {
10
+ if (key.startsWith("$")) continue;
11
+ const next = [...path, key];
12
+ if (isToken(member)) {
13
+ flat.set(next.join("."), { token: member, type: member.$type ?? groupType });
14
+ } else if (isGroup(member)) {
15
+ walk(member, next, groupType);
16
+ }
17
+ }
18
+ };
19
+ walk(document, []);
20
+ const resolving = /* @__PURE__ */ new Set();
21
+ const resolved = /* @__PURE__ */ new Map();
22
+ const valueOf = (name) => {
23
+ if (resolved.has(name)) return resolved.get(name);
24
+ const entry = flat.get(name);
25
+ if (!entry) throw new Error(`Design token alias "{${name}}" does not resolve to a declared token.`);
26
+ if (resolving.has(name)) {
27
+ throw new Error(`Design token alias cycle: ${[...resolving, name].join(" -> ")}.`);
28
+ }
29
+ const raw = entry.token.$value;
30
+ if (typeof raw !== "string") {
31
+ resolved.set(name, raw);
32
+ return raw;
33
+ }
34
+ const alias = ALIAS.exec(raw.trim());
35
+ if (!alias) {
36
+ resolved.set(name, raw);
37
+ return raw;
38
+ }
39
+ resolving.add(name);
40
+ const target = valueOf(alias[1]);
41
+ resolving.delete(name);
42
+ resolved.set(name, target);
43
+ return target;
44
+ };
45
+ return [...flat.entries()].map(([name, entry]) => {
46
+ const value = valueOf(name);
47
+ return {
48
+ name,
49
+ value,
50
+ ...entry.type === void 0 ? {} : { type: entry.type },
51
+ ...entry.token.$description === void 0 ? {} : { description: entry.token.$description }
52
+ };
53
+ });
54
+ }
55
+ function validatePortRequirements(input) {
56
+ const findings = [];
57
+ for (const requirement of input.capability.requirements?.ports ?? []) {
58
+ const candidates = input.adapters.filter((adapter) => adapter.port === requirement.name);
59
+ if (candidates.length === 0) {
60
+ findings.push({
61
+ code: "unsatisfied_port",
62
+ port: requirement.name,
63
+ message: `capability "${input.capability.namespace}" requires port "${requirement.name}" and no adapter is registered for it`
64
+ });
65
+ continue;
66
+ }
67
+ if (requirement.standard) {
68
+ const speaking = candidates.filter((adapter) => adapter.standard?.name === requirement.standard.name);
69
+ if (speaking.length === 0) {
70
+ findings.push({
71
+ code: "standard_not_spoken",
72
+ port: requirement.name,
73
+ message: `port "${requirement.name}" must speak "${requirement.standard.name}"; registered adapters are ${candidates.map((adapter) => `${adapter.vendor}(${adapter.standard?.name ?? "no standard"})`).join(", ")}`
74
+ });
75
+ continue;
76
+ }
77
+ }
78
+ if (requirement.version && !candidates.some((adapter) => adapter.version === requirement.version)) {
79
+ findings.push({
80
+ code: "version_mismatch",
81
+ port: requirement.name,
82
+ message: `port "${requirement.name}" requires version "${requirement.version}"; registered adapters are ${candidates.map((adapter) => `${adapter.vendor}@${adapter.version ?? "unversioned"}`).join(", ")}`
83
+ });
84
+ }
85
+ }
86
+ return { valid: findings.length === 0, findings };
87
+ }
88
+ function assertPortRequirementsSatisfied(input) {
89
+ const result = validatePortRequirements(input);
90
+ if (result.valid) return;
91
+ throw new Error([
92
+ `Port requirements for "${input.capability.namespace}" are not satisfied:`,
93
+ ...result.findings.map((finding) => ` - ${finding.message}`)
94
+ ].join("\n"));
95
+ }
96
+ var PortContractFailure = class extends Error {
97
+ name = "PortContractFailure";
98
+ };
99
+ function expect(condition, message) {
100
+ if (!condition) throw new PortContractFailure(message);
101
+ }
102
+ function flagsPortChecks() {
103
+ const unknownKey = "fabric.contract.definitely-not-configured";
104
+ return [
105
+ {
106
+ id: "flags.default-on-unknown-key",
107
+ title: "an unknown flag resolves to the supplied default rather than throwing",
108
+ async run(port) {
109
+ const result = await port.resolveBoolean(unknownKey, true);
110
+ expect(result.value === true, `an unknown flag returned ${String(result.value)} instead of the supplied default`);
111
+ expect(
112
+ result.reason === "DEFAULT" || result.reason === "ERROR",
113
+ `an unknown flag resolved with reason "${result.reason}"; a default or error was expected`
114
+ );
115
+ }
116
+ },
117
+ {
118
+ id: "flags.default-is-typed",
119
+ title: "each typed resolver returns its own type",
120
+ async run(port) {
121
+ expect(typeof (await port.resolveBoolean(unknownKey, false)).value === "boolean", "resolveBoolean did not return a boolean");
122
+ expect(typeof (await port.resolveString(unknownKey, "fallback")).value === "string", "resolveString did not return a string");
123
+ expect(typeof (await port.resolveNumber(unknownKey, 42)).value === "number", "resolveNumber did not return a number");
124
+ }
125
+ },
126
+ {
127
+ id: "flags.evaluation-is-pure",
128
+ title: "the same key and context resolve the same way twice",
129
+ async run(port) {
130
+ const context = { targetingKey: "fabric-contract-subject" };
131
+ const first = await port.resolveString(unknownKey, "fallback", context);
132
+ const second = await port.resolveString(unknownKey, "fallback", context);
133
+ expect(first.value === second.value, `the same evaluation returned "${first.value}" then "${second.value}"`);
134
+ }
135
+ },
136
+ {
137
+ id: "flags.tolerates-absent-context",
138
+ title: "evaluation without a context does not throw",
139
+ async run(port) {
140
+ await port.resolveBoolean(unknownKey, false);
141
+ }
142
+ }
143
+ ];
144
+ }
145
+ function designTokensPortChecks(theme) {
146
+ return [
147
+ {
148
+ id: "tokens.theme-resolves",
149
+ title: "a known theme resolves to at least one token",
150
+ async run(port) {
151
+ const tokens = await port.resolve(theme);
152
+ expect(Array.isArray(tokens) && tokens.length > 0, `theme "${theme}" resolved to no tokens`);
153
+ }
154
+ },
155
+ {
156
+ id: "tokens.no-unresolved-aliases",
157
+ title: "no resolved token still carries an alias",
158
+ async run(port) {
159
+ for (const token of await port.resolve(theme)) {
160
+ expect(
161
+ typeof token.value !== "string" || !ALIAS.test(token.value.trim()),
162
+ `token "${token.name}" resolved to the unfollowed alias ${String(token.value)}`
163
+ );
164
+ }
165
+ }
166
+ },
167
+ {
168
+ id: "tokens.names-are-unique",
169
+ title: "token names are unique within a theme",
170
+ async run(port) {
171
+ const names = (await port.resolve(theme)).map((token) => token.name);
172
+ expect(new Set(names).size === names.length, "the theme resolved duplicate token names");
173
+ }
174
+ }
175
+ ];
176
+ }
177
+ async function runPortContract(port, checks) {
178
+ const failures = [];
179
+ for (const check of checks) {
180
+ try {
181
+ await check.run(port);
182
+ } catch (error) {
183
+ failures.push({ id: check.id, message: error instanceof Error ? error.message : String(error) });
184
+ }
185
+ }
186
+ return { passed: failures.length === 0, failures };
187
+ }
188
+ export {
189
+ assertPortRequirementsSatisfied,
190
+ designTokensPortChecks,
191
+ flagsPortChecks,
192
+ resolveDesignTokens,
193
+ runPortContract,
194
+ validatePortRequirements
195
+ };
196
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../index.ts"],"sourcesContent":["/**\n * Port interfaces are declared here and implemented by vendor adapters elsewhere.\n * Nothing in this package imports a vendor SDK: a port is a shape, and an adapter\n * is anything that satisfies it. Where an open standard exists the port speaks it,\n * so the incumbent vendor is already just one provider behind the interface.\n */\n\n// ── Feature flags (OpenFeature-shaped) ──────────────────────────────────────\n\nexport type FlagValue = boolean | string | number | { [key: string]: unknown };\n\nexport interface EvaluationContext {\n\t/** Stable identifier for the subject of evaluation, usually a tenant or actor. */\n\ttargetingKey?: string;\n\t[attribute: string]: unknown;\n}\n\nexport type ResolutionReason = \"STATIC\" | \"DEFAULT\" | \"TARGETING_MATCH\" | \"SPLIT\" | \"CACHED\" | \"ERROR\";\n\nexport interface ResolutionDetails<T extends FlagValue> {\n\tvalue: T;\n\treason: ResolutionReason;\n\tvariant?: string;\n\terrorCode?: string;\n}\n\n/**\n * Structurally compatible with an OpenFeature provider's evaluation surface, so an\n * OpenFeature provider is a thin adapter rather than a translation layer.\n */\nexport interface FlagsPort {\n\tresolveBoolean(flagKey: string, defaultValue: boolean, context?: EvaluationContext): Promise<ResolutionDetails<boolean>>;\n\tresolveString(flagKey: string, defaultValue: string, context?: EvaluationContext): Promise<ResolutionDetails<string>>;\n\tresolveNumber(flagKey: string, defaultValue: number, context?: EvaluationContext): Promise<ResolutionDetails<number>>;\n}\n\n// ── Design tokens (W3C DTCG) ────────────────────────────────────────────────\n\nexport interface DtcgToken {\n\t$value: unknown;\n\t$type?: string;\n\t$description?: string;\n}\n\nexport interface DtcgGroup {\n\t$type?: string;\n\t$description?: string;\n\t[member: string]: DtcgToken | DtcgGroup | string | undefined;\n}\n\nexport interface ResolvedToken {\n\tname: string;\n\tvalue: unknown;\n\ttype?: string;\n\tdescription?: string;\n}\n\nexport interface DesignTokensPort {\n\t/** Resolved tokens for one theme. Aliases are already followed. */\n\tresolve(theme: string): Promise<ResolvedToken[]>;\n}\n\nconst ALIAS = /^\\{([^}]+)\\}$/;\n\nconst isToken = (value: unknown): value is DtcgToken =>\n\t!!value && typeof value === \"object\" && !Array.isArray(value) && \"$value\" in value;\n\nconst isGroup = (value: unknown): value is DtcgGroup =>\n\t!!value && typeof value === \"object\" && !Array.isArray(value) && !(\"$value\" in value);\n\n/**\n * Flattens a DTCG document and follows aliases. `$type` is inherited from the\n * nearest ancestor group that declares one, which is what the format specifies\n * and what makes a theme file readable.\n */\nexport function resolveDesignTokens(document: DtcgGroup): ResolvedToken[] {\n\tconst flat = new Map<string, { token: DtcgToken; type?: string }>();\n\n\tconst walk = (node: DtcgGroup, path: string[], inheritedType?: string): void => {\n\t\tconst groupType = typeof node.$type === \"string\" ? node.$type : inheritedType;\n\t\tfor (const [key, member] of Object.entries(node)) {\n\t\t\tif (key.startsWith(\"$\")) continue;\n\t\t\tconst next = [...path, key];\n\t\t\tif (isToken(member)) {\n\t\t\t\tflat.set(next.join(\".\"), { token: member, type: member.$type ?? groupType });\n\t\t\t} else if (isGroup(member)) {\n\t\t\t\twalk(member, next, groupType);\n\t\t\t}\n\t\t}\n\t};\n\twalk(document, []);\n\n\tconst resolving = new Set<string>();\n\tconst resolved = new Map<string, unknown>();\n\n\tconst valueOf = (name: string): unknown => {\n\t\tif (resolved.has(name)) return resolved.get(name);\n\t\tconst entry = flat.get(name);\n\t\tif (!entry) throw new Error(`Design token alias \"{${name}}\" does not resolve to a declared token.`);\n\t\tif (resolving.has(name)) {\n\t\t\tthrow new Error(`Design token alias cycle: ${[...resolving, name].join(\" -> \")}.`);\n\t\t}\n\t\tconst raw = entry.token.$value;\n\t\tif (typeof raw !== \"string\") {\n\t\t\tresolved.set(name, raw);\n\t\t\treturn raw;\n\t\t}\n\t\tconst alias = ALIAS.exec(raw.trim());\n\t\tif (!alias) {\n\t\t\tresolved.set(name, raw);\n\t\t\treturn raw;\n\t\t}\n\t\tresolving.add(name);\n\t\tconst target = valueOf(alias[1]!);\n\t\tresolving.delete(name);\n\t\tresolved.set(name, target);\n\t\treturn target;\n\t};\n\n\treturn [...flat.entries()].map(([name, entry]) => {\n\t\tconst value = valueOf(name);\n\t\treturn {\n\t\t\tname,\n\t\t\tvalue,\n\t\t\t...(entry.type === undefined ? {} : { type: entry.type }),\n\t\t\t...(entry.token.$description === undefined ? {} : { description: entry.token.$description }),\n\t\t};\n\t});\n}\n\n// ── Requirement satisfaction ────────────────────────────────────────────────\n\nexport interface PortRequirement {\n\tname: string;\n\tversion?: string;\n\tstandard?: { name: string; version?: string };\n}\n\n/** The subset of capability metadata that declares what ports it needs. */\nexport interface PortRequiringCapability {\n\tnamespace: string;\n\trequirements?: { ports?: PortRequirement[] };\n}\n\nexport interface RegisteredAdapter {\n\t/** Port this adapter implements, matching the requirement's `name`. */\n\tport: string;\n\tvendor: string;\n\tversion?: string;\n\tstandard?: { name: string; version?: string };\n}\n\nexport type PortFindingCode = \"unsatisfied_port\" | \"standard_not_spoken\" | \"version_mismatch\";\n\nexport interface PortFinding {\n\tcode: PortFindingCode;\n\tport: string;\n\tmessage: string;\n}\n\nexport interface PortValidationResult {\n\tvalid: boolean;\n\tfindings: PortFinding[];\n}\n\n/**\n * Checks that every port a capability declares is backed by a registered adapter.\n * `CapabilityPortRequirement` is otherwise a name that resolves against nothing;\n * this is what turns the declaration into a deployment-time gate.\n *\n * Version matching is exact. Range negotiation belongs to whatever installs the\n * adapters, and pretending to do semver here would be worse than not doing it.\n */\nexport function validatePortRequirements(input: {\n\tcapability: PortRequiringCapability;\n\tadapters: readonly RegisteredAdapter[];\n}): PortValidationResult {\n\tconst findings: PortFinding[] = [];\n\tfor (const requirement of input.capability.requirements?.ports ?? []) {\n\t\tconst candidates = input.adapters.filter((adapter) => adapter.port === requirement.name);\n\t\tif (candidates.length === 0) {\n\t\t\tfindings.push({\n\t\t\t\tcode: \"unsatisfied_port\",\n\t\t\t\tport: requirement.name,\n\t\t\t\tmessage: `capability \"${input.capability.namespace}\" requires port \"${requirement.name}\" and no adapter is registered for it`,\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tif (requirement.standard) {\n\t\t\tconst speaking = candidates.filter((adapter) => adapter.standard?.name === requirement.standard!.name);\n\t\t\tif (speaking.length === 0) {\n\t\t\t\tfindings.push({\n\t\t\t\t\tcode: \"standard_not_spoken\",\n\t\t\t\t\tport: requirement.name,\n\t\t\t\t\tmessage: `port \"${requirement.name}\" must speak \"${requirement.standard.name}\"; registered adapters are ${candidates.map((adapter) => `${adapter.vendor}(${adapter.standard?.name ?? \"no standard\"})`).join(\", \")}`,\n\t\t\t\t});\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tif (requirement.version && !candidates.some((adapter) => adapter.version === requirement.version)) {\n\t\t\tfindings.push({\n\t\t\t\tcode: \"version_mismatch\",\n\t\t\t\tport: requirement.name,\n\t\t\t\tmessage: `port \"${requirement.name}\" requires version \"${requirement.version}\"; registered adapters are ${candidates.map((adapter) => `${adapter.vendor}@${adapter.version ?? \"unversioned\"}`).join(\", \")}`,\n\t\t\t});\n\t\t}\n\t}\n\treturn { valid: findings.length === 0, findings };\n}\n\nexport function assertPortRequirementsSatisfied(input: {\n\tcapability: PortRequiringCapability;\n\tadapters: readonly RegisteredAdapter[];\n}): void {\n\tconst result = validatePortRequirements(input);\n\tif (result.valid) return;\n\tthrow new Error([\n\t\t`Port requirements for \"${input.capability.namespace}\" are not satisfied:`,\n\t\t...result.findings.map((finding) => ` - ${finding.message}`),\n\t].join(\"\\n\"));\n}\n\n// ── Adapter contract kit ────────────────────────────────────────────────────\n\nexport interface PortCheck<TPort> {\n\tid: string;\n\ttitle: string;\n\t/** Throws on failure, exactly as an assertion would. */\n\trun(port: TPort): Promise<void>;\n}\n\nclass PortContractFailure extends Error {\n\toverride readonly name = \"PortContractFailure\";\n}\n\nfunction expect(condition: unknown, message: string): asserts condition {\n\tif (!condition) throw new PortContractFailure(message);\n}\n\n/**\n * Every flags adapter must pass these, so swapping vendors is an adapter build\n * plus a configuration change rather than a migration.\n */\nexport function flagsPortChecks(): PortCheck<FlagsPort>[] {\n\tconst unknownKey = \"fabric.contract.definitely-not-configured\";\n\treturn [\n\t\t{\n\t\t\tid: \"flags.default-on-unknown-key\",\n\t\t\ttitle: \"an unknown flag resolves to the supplied default rather than throwing\",\n\t\t\tasync run(port) {\n\t\t\t\tconst result = await port.resolveBoolean(unknownKey, true);\n\t\t\t\texpect(result.value === true, `an unknown flag returned ${String(result.value)} instead of the supplied default`);\n\t\t\t\texpect(\n\t\t\t\t\tresult.reason === \"DEFAULT\" || result.reason === \"ERROR\",\n\t\t\t\t\t`an unknown flag resolved with reason \"${result.reason}\"; a default or error was expected`,\n\t\t\t\t);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"flags.default-is-typed\",\n\t\t\ttitle: \"each typed resolver returns its own type\",\n\t\t\tasync run(port) {\n\t\t\t\texpect(typeof (await port.resolveBoolean(unknownKey, false)).value === \"boolean\", \"resolveBoolean did not return a boolean\");\n\t\t\t\texpect(typeof (await port.resolveString(unknownKey, \"fallback\")).value === \"string\", \"resolveString did not return a string\");\n\t\t\t\texpect(typeof (await port.resolveNumber(unknownKey, 42)).value === \"number\", \"resolveNumber did not return a number\");\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"flags.evaluation-is-pure\",\n\t\t\ttitle: \"the same key and context resolve the same way twice\",\n\t\t\tasync run(port) {\n\t\t\t\tconst context = { targetingKey: \"fabric-contract-subject\" };\n\t\t\t\tconst first = await port.resolveString(unknownKey, \"fallback\", context);\n\t\t\t\tconst second = await port.resolveString(unknownKey, \"fallback\", context);\n\t\t\t\texpect(first.value === second.value, `the same evaluation returned \"${first.value}\" then \"${second.value}\"`);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"flags.tolerates-absent-context\",\n\t\t\ttitle: \"evaluation without a context does not throw\",\n\t\t\tasync run(port) {\n\t\t\t\tawait port.resolveBoolean(unknownKey, false);\n\t\t\t},\n\t\t},\n\t];\n}\n\nexport function designTokensPortChecks(theme: string): PortCheck<DesignTokensPort>[] {\n\treturn [\n\t\t{\n\t\t\tid: \"tokens.theme-resolves\",\n\t\t\ttitle: \"a known theme resolves to at least one token\",\n\t\t\tasync run(port) {\n\t\t\t\tconst tokens = await port.resolve(theme);\n\t\t\t\texpect(Array.isArray(tokens) && tokens.length > 0, `theme \"${theme}\" resolved to no tokens`);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"tokens.no-unresolved-aliases\",\n\t\t\ttitle: \"no resolved token still carries an alias\",\n\t\t\tasync run(port) {\n\t\t\t\tfor (const token of await port.resolve(theme)) {\n\t\t\t\t\texpect(\n\t\t\t\t\t\ttypeof token.value !== \"string\" || !ALIAS.test(token.value.trim()),\n\t\t\t\t\t\t`token \"${token.name}\" resolved to the unfollowed alias ${String(token.value)}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"tokens.names-are-unique\",\n\t\t\ttitle: \"token names are unique within a theme\",\n\t\t\tasync run(port) {\n\t\t\t\tconst names = (await port.resolve(theme)).map((token) => token.name);\n\t\t\t\texpect(new Set(names).size === names.length, \"the theme resolved duplicate token names\");\n\t\t\t},\n\t\t},\n\t];\n}\n\n/** Runs a contract suite and returns every failure, rather than stopping at the first. */\nexport async function runPortContract<TPort>(port: TPort, checks: readonly PortCheck<TPort>[]): Promise<{ passed: boolean; failures: Array<{ id: string; message: string }> }> {\n\tconst failures: Array<{ id: string; message: string }> = [];\n\tfor (const check of checks) {\n\t\ttry {\n\t\t\tawait check.run(port);\n\t\t} catch (error) {\n\t\t\tfailures.push({ id: check.id, message: error instanceof Error ? error.message : String(error) });\n\t\t}\n\t}\n\treturn { passed: failures.length === 0, failures };\n}\n"],"mappings":";AA8DA,IAAM,QAAQ;AAEd,IAAM,UAAU,CAAC,UAChB,CAAC,CAAC,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,KAAK,YAAY;AAE9E,IAAM,UAAU,CAAC,UAChB,CAAC,CAAC,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,KAAK,EAAE,YAAY;AAOzE,SAAS,oBAAoB,UAAsC;AACzE,QAAM,OAAO,oBAAI,IAAiD;AAElE,QAAM,OAAO,CAAC,MAAiB,MAAgB,kBAAiC;AAC/E,UAAM,YAAY,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ;AAChE,eAAW,CAAC,KAAK,MAAM,KAAK,OAAO,QAAQ,IAAI,GAAG;AACjD,UAAI,IAAI,WAAW,GAAG,EAAG;AACzB,YAAM,OAAO,CAAC,GAAG,MAAM,GAAG;AAC1B,UAAI,QAAQ,MAAM,GAAG;AACpB,aAAK,IAAI,KAAK,KAAK,GAAG,GAAG,EAAE,OAAO,QAAQ,MAAM,OAAO,SAAS,UAAU,CAAC;AAAA,MAC5E,WAAW,QAAQ,MAAM,GAAG;AAC3B,aAAK,QAAQ,MAAM,SAAS;AAAA,MAC7B;AAAA,IACD;AAAA,EACD;AACA,OAAK,UAAU,CAAC,CAAC;AAEjB,QAAM,YAAY,oBAAI,IAAY;AAClC,QAAM,WAAW,oBAAI,IAAqB;AAE1C,QAAM,UAAU,CAAC,SAA0B;AAC1C,QAAI,SAAS,IAAI,IAAI,EAAG,QAAO,SAAS,IAAI,IAAI;AAChD,UAAM,QAAQ,KAAK,IAAI,IAAI;AAC3B,QAAI,CAAC,MAAO,OAAM,IAAI,MAAM,wBAAwB,IAAI,0CAA0C;AAClG,QAAI,UAAU,IAAI,IAAI,GAAG;AACxB,YAAM,IAAI,MAAM,6BAA6B,CAAC,GAAG,WAAW,IAAI,EAAE,KAAK,MAAM,CAAC,GAAG;AAAA,IAClF;AACA,UAAM,MAAM,MAAM,MAAM;AACxB,QAAI,OAAO,QAAQ,UAAU;AAC5B,eAAS,IAAI,MAAM,GAAG;AACtB,aAAO;AAAA,IACR;AACA,UAAM,QAAQ,MAAM,KAAK,IAAI,KAAK,CAAC;AACnC,QAAI,CAAC,OAAO;AACX,eAAS,IAAI,MAAM,GAAG;AACtB,aAAO;AAAA,IACR;AACA,cAAU,IAAI,IAAI;AAClB,UAAM,SAAS,QAAQ,MAAM,CAAC,CAAE;AAChC,cAAU,OAAO,IAAI;AACrB,aAAS,IAAI,MAAM,MAAM;AACzB,WAAO;AAAA,EACR;AAEA,SAAO,CAAC,GAAG,KAAK,QAAQ,CAAC,EAAE,IAAI,CAAC,CAAC,MAAM,KAAK,MAAM;AACjD,UAAM,QAAQ,QAAQ,IAAI;AAC1B,WAAO;AAAA,MACN;AAAA,MACA;AAAA,MACA,GAAI,MAAM,SAAS,SAAY,CAAC,IAAI,EAAE,MAAM,MAAM,KAAK;AAAA,MACvD,GAAI,MAAM,MAAM,iBAAiB,SAAY,CAAC,IAAI,EAAE,aAAa,MAAM,MAAM,aAAa;AAAA,IAC3F;AAAA,EACD,CAAC;AACF;AA6CO,SAAS,yBAAyB,OAGhB;AACxB,QAAM,WAA0B,CAAC;AACjC,aAAW,eAAe,MAAM,WAAW,cAAc,SAAS,CAAC,GAAG;AACrE,UAAM,aAAa,MAAM,SAAS,OAAO,CAAC,YAAY,QAAQ,SAAS,YAAY,IAAI;AACvF,QAAI,WAAW,WAAW,GAAG;AAC5B,eAAS,KAAK;AAAA,QACb,MAAM;AAAA,QACN,MAAM,YAAY;AAAA,QAClB,SAAS,eAAe,MAAM,WAAW,SAAS,oBAAoB,YAAY,IAAI;AAAA,MACvF,CAAC;AACD;AAAA,IACD;AACA,QAAI,YAAY,UAAU;AACzB,YAAM,WAAW,WAAW,OAAO,CAAC,YAAY,QAAQ,UAAU,SAAS,YAAY,SAAU,IAAI;AACrG,UAAI,SAAS,WAAW,GAAG;AAC1B,iBAAS,KAAK;AAAA,UACb,MAAM;AAAA,UACN,MAAM,YAAY;AAAA,UAClB,SAAS,SAAS,YAAY,IAAI,iBAAiB,YAAY,SAAS,IAAI,8BAA8B,WAAW,IAAI,CAAC,YAAY,GAAG,QAAQ,MAAM,IAAI,QAAQ,UAAU,QAAQ,aAAa,GAAG,EAAE,KAAK,IAAI,CAAC;AAAA,QAClN,CAAC;AACD;AAAA,MACD;AAAA,IACD;AACA,QAAI,YAAY,WAAW,CAAC,WAAW,KAAK,CAAC,YAAY,QAAQ,YAAY,YAAY,OAAO,GAAG;AAClG,eAAS,KAAK;AAAA,QACb,MAAM;AAAA,QACN,MAAM,YAAY;AAAA,QAClB,SAAS,SAAS,YAAY,IAAI,uBAAuB,YAAY,OAAO,8BAA8B,WAAW,IAAI,CAAC,YAAY,GAAG,QAAQ,MAAM,IAAI,QAAQ,WAAW,aAAa,EAAE,EAAE,KAAK,IAAI,CAAC;AAAA,MAC1M,CAAC;AAAA,IACF;AAAA,EACD;AACA,SAAO,EAAE,OAAO,SAAS,WAAW,GAAG,SAAS;AACjD;AAEO,SAAS,gCAAgC,OAGvC;AACR,QAAM,SAAS,yBAAyB,KAAK;AAC7C,MAAI,OAAO,MAAO;AAClB,QAAM,IAAI,MAAM;AAAA,IACf,0BAA0B,MAAM,WAAW,SAAS;AAAA,IACpD,GAAG,OAAO,SAAS,IAAI,CAAC,YAAY,OAAO,QAAQ,OAAO,EAAE;AAAA,EAC7D,EAAE,KAAK,IAAI,CAAC;AACb;AAWA,IAAM,sBAAN,cAAkC,MAAM;AAAA,EACrB,OAAO;AAC1B;AAEA,SAAS,OAAO,WAAoB,SAAoC;AACvE,MAAI,CAAC,UAAW,OAAM,IAAI,oBAAoB,OAAO;AACtD;AAMO,SAAS,kBAA0C;AACzD,QAAM,aAAa;AACnB,SAAO;AAAA,IACN;AAAA,MACC,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM,IAAI,MAAM;AACf,cAAM,SAAS,MAAM,KAAK,eAAe,YAAY,IAAI;AACzD,eAAO,OAAO,UAAU,MAAM,4BAA4B,OAAO,OAAO,KAAK,CAAC,kCAAkC;AAChH;AAAA,UACC,OAAO,WAAW,aAAa,OAAO,WAAW;AAAA,UACjD,yCAAyC,OAAO,MAAM;AAAA,QACvD;AAAA,MACD;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM,IAAI,MAAM;AACf,eAAO,QAAQ,MAAM,KAAK,eAAe,YAAY,KAAK,GAAG,UAAU,WAAW,yCAAyC;AAC3H,eAAO,QAAQ,MAAM,KAAK,cAAc,YAAY,UAAU,GAAG,UAAU,UAAU,uCAAuC;AAC5H,eAAO,QAAQ,MAAM,KAAK,cAAc,YAAY,EAAE,GAAG,UAAU,UAAU,uCAAuC;AAAA,MACrH;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM,IAAI,MAAM;AACf,cAAM,UAAU,EAAE,cAAc,0BAA0B;AAC1D,cAAM,QAAQ,MAAM,KAAK,cAAc,YAAY,YAAY,OAAO;AACtE,cAAM,SAAS,MAAM,KAAK,cAAc,YAAY,YAAY,OAAO;AACvE,eAAO,MAAM,UAAU,OAAO,OAAO,iCAAiC,MAAM,KAAK,WAAW,OAAO,KAAK,GAAG;AAAA,MAC5G;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM,IAAI,MAAM;AACf,cAAM,KAAK,eAAe,YAAY,KAAK;AAAA,MAC5C;AAAA,IACD;AAAA,EACD;AACD;AAEO,SAAS,uBAAuB,OAA8C;AACpF,SAAO;AAAA,IACN;AAAA,MACC,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM,IAAI,MAAM;AACf,cAAM,SAAS,MAAM,KAAK,QAAQ,KAAK;AACvC,eAAO,MAAM,QAAQ,MAAM,KAAK,OAAO,SAAS,GAAG,UAAU,KAAK,yBAAyB;AAAA,MAC5F;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM,IAAI,MAAM;AACf,mBAAW,SAAS,MAAM,KAAK,QAAQ,KAAK,GAAG;AAC9C;AAAA,YACC,OAAO,MAAM,UAAU,YAAY,CAAC,MAAM,KAAK,MAAM,MAAM,KAAK,CAAC;AAAA,YACjE,UAAU,MAAM,IAAI,sCAAsC,OAAO,MAAM,KAAK,CAAC;AAAA,UAC9E;AAAA,QACD;AAAA,MACD;AAAA,IACD;AAAA,IACA;AAAA,MACC,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM,IAAI,MAAM;AACf,cAAM,SAAS,MAAM,KAAK,QAAQ,KAAK,GAAG,IAAI,CAAC,UAAU,MAAM,IAAI;AACnE,eAAO,IAAI,IAAI,KAAK,EAAE,SAAS,MAAM,QAAQ,0CAA0C;AAAA,MACxF;AAAA,IACD;AAAA,EACD;AACD;AAGA,eAAsB,gBAAuB,MAAa,QAAqH;AAC9K,QAAM,WAAmD,CAAC;AAC1D,aAAW,SAAS,QAAQ;AAC3B,QAAI;AACH,YAAM,MAAM,IAAI,IAAI;AAAA,IACrB,SAAS,OAAO;AACf,eAAS,KAAK,EAAE,IAAI,MAAM,IAAI,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,CAAC;AAAA,IAChG;AAAA,EACD;AACA,SAAO,EAAE,QAAQ,SAAS,WAAW,GAAG,SAAS;AAClD;","names":[]}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@fabricorg/ports",
3
+ "version": "0.2.0",
4
+ "description": "Vendor-neutral port interfaces, a DTCG token resolver, and the contract-test kit every adapter must pass.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "main": "./dist/index.cjs",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "require": {
17
+ "types": "./dist/index.d.cts",
18
+ "default": "./dist/index.cjs"
19
+ }
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "README.md",
25
+ "CHANGELOG.md",
26
+ "LICENSE"
27
+ ],
28
+ "devDependencies": {
29
+ "@types/node": "^22.10.0",
30
+ "tsup": "^8.5.0",
31
+ "typescript": "^5.7.3",
32
+ "vitest": "^4.1.5"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "scripts": {
38
+ "clean": "rm -rf dist tsconfig.tsbuildinfo",
39
+ "build": "tsup",
40
+ "type-check": "tsc --noEmit",
41
+ "test": "vitest run"
42
+ }
43
+ }