@nhtio/adk 0.1.0-master-bfedd81c → 0.1.0-master-0e75bb0f
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/batteries/media/engines/audio_decode.cjs.map +1 -1
- package/batteries/media/engines/audio_decode.d.ts +7 -3
- package/batteries/media/engines/audio_decode.mjs.map +1 -1
- package/batteries/media/index.d.ts +2 -2
- package/batteries/media/lint.cjs +1 -1
- package/batteries/media/lint.cjs.map +1 -1
- package/batteries/media/lint.d.ts +1 -1
- package/batteries/media/lint.mjs +1 -1
- package/batteries/media/lint.mjs.map +1 -1
- package/batteries/media/pipe.d.ts +1 -1
- package/batteries/media.cjs +1 -1
- package/batteries/media.cjs.map +1 -1
- package/batteries/media.mjs +1 -1
- package/batteries/media.mjs.map +1 -1
- package/batteries/tools/scrapper/index.d.ts +1 -0
- package/batteries/tools/scrapper.cjs +1 -1
- package/batteries/tools/scrapper.mjs +1 -1
- package/batteries/tools/searxng.cjs +1 -1
- package/batteries/tools/searxng.mjs +1 -1
- package/batteries/tools/web_retrieval/index.d.ts +4 -4
- package/batteries/tools/web_retrieval.cjs +3 -3
- package/batteries/tools/web_retrieval.cjs.map +1 -1
- package/batteries/tools/web_retrieval.mjs +3 -3
- package/batteries/tools/web_retrieval.mjs.map +1 -1
- package/batteries/tools.cjs +2 -2
- package/batteries/tools.mjs +2 -2
- package/batteries/vector/arangodb.cjs +1 -1
- package/batteries/vector/arangodb.mjs +1 -1
- package/batteries/vector/builder.cjs +1 -1
- package/batteries/vector/builder.mjs +1 -1
- package/batteries/vector/chroma.cjs +2 -2
- package/batteries/vector/chroma.mjs +2 -2
- package/batteries/vector/clickhouse.cjs +2 -2
- package/batteries/vector/clickhouse.mjs +2 -2
- package/batteries/vector/cloudflare.cjs +2 -2
- package/batteries/vector/cloudflare.mjs +2 -2
- package/batteries/vector/couchbase.cjs +2 -2
- package/batteries/vector/couchbase.mjs +2 -2
- package/batteries/vector/duckdb.cjs +2 -2
- package/batteries/vector/duckdb.mjs +2 -2
- package/batteries/vector/elasticsearch.cjs +2 -2
- package/batteries/vector/elasticsearch.mjs +2 -2
- package/batteries/vector/hnswlib.cjs +2 -2
- package/batteries/vector/hnswlib.mjs +2 -2
- package/batteries/vector/in_memory.cjs +2 -2
- package/batteries/vector/in_memory.mjs +2 -2
- package/batteries/vector/lancedb.cjs +2 -2
- package/batteries/vector/lancedb.mjs +2 -2
- package/batteries/vector/mariadb.cjs +2 -2
- package/batteries/vector/mariadb.mjs +2 -2
- package/batteries/vector/meilisearch.cjs +1 -1
- package/batteries/vector/meilisearch.mjs +1 -1
- package/batteries/vector/milvus.cjs +2 -2
- package/batteries/vector/milvus.mjs +2 -2
- package/batteries/vector/mongodb.cjs +1 -1
- package/batteries/vector/mongodb.mjs +1 -1
- package/batteries/vector/neo4j.cjs +1 -1
- package/batteries/vector/neo4j.mjs +1 -1
- package/batteries/vector/opensearch.cjs +2 -2
- package/batteries/vector/opensearch.mjs +2 -2
- package/batteries/vector/oracle23ai.cjs +2 -2
- package/batteries/vector/oracle23ai.mjs +2 -2
- package/batteries/vector/orama.cjs +2 -2
- package/batteries/vector/orama.mjs +2 -2
- package/batteries/vector/pgvector.cjs +2 -2
- package/batteries/vector/pgvector.mjs +2 -2
- package/batteries/vector/pinecone.cjs +2 -2
- package/batteries/vector/pinecone.mjs +2 -2
- package/batteries/vector/qdrant.cjs +2 -2
- package/batteries/vector/qdrant.mjs +2 -2
- package/batteries/vector/redis.cjs +2 -2
- package/batteries/vector/redis.mjs +2 -2
- package/batteries/vector/s3vectors.cjs +2 -2
- package/batteries/vector/s3vectors.mjs +2 -2
- package/batteries/vector/solr.cjs +1 -1
- package/batteries/vector/solr.mjs +1 -1
- package/batteries/vector/sqlite_vec.cjs +2 -2
- package/batteries/vector/sqlite_vec.mjs +2 -2
- package/batteries/vector/surrealdb.cjs +1 -1
- package/batteries/vector/surrealdb.mjs +1 -1
- package/batteries/vector/typesense.cjs +2 -2
- package/batteries/vector/typesense.mjs +2 -2
- package/batteries/vector/validation.cjs +1 -1
- package/batteries/vector/validation.mjs +1 -1
- package/batteries/vector/vespa.cjs +2 -2
- package/batteries/vector/vespa.mjs +2 -2
- package/batteries/vector/weaviate.cjs +2 -2
- package/batteries/vector/weaviate.mjs +2 -2
- package/batteries/vector.cjs +3 -3
- package/batteries/vector.mjs +3 -3
- package/batteries.cjs +16 -16
- package/batteries.mjs +16 -16
- package/eslint.cjs +1 -1
- package/eslint.mjs +1 -1
- package/index.cjs +2 -2
- package/index.mjs +2 -2
- package/mcp/adk-docs-corpus.json +1 -1
- package/package.json +172 -172
- package/{scrapper-BXlRzMaG.js → scrapper-7DB7dSae.js} +2 -2
- package/{scrapper-BXlRzMaG.js.map → scrapper-7DB7dSae.js.map} +1 -1
- package/{scrapper-DKy8Cx3L.mjs → scrapper-DEEbpQYp.mjs} +2 -2
- package/{scrapper-DKy8Cx3L.mjs.map → scrapper-DEEbpQYp.mjs.map} +1 -1
- package/{searxng-Cxsfv_sr.mjs → searxng-CHivYbnV.mjs} +2 -2
- package/{searxng-Cxsfv_sr.mjs.map → searxng-CHivYbnV.mjs.map} +1 -1
- package/{searxng-DMV9TpMv.js → searxng-D0igol3B.js} +2 -2
- package/{searxng-DMV9TpMv.js.map → searxng-D0igol3B.js.map} +1 -1
- package/skills/adk-assembly/SKILL.md +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audio_decode.cjs","names":[],"sources":["../../../../src/batteries/media/engines/audio_decode.ts"],"sourcesContent":["/**\n * A cross-environment audio-decoding {@link @nhtio/adk/batteries/media/contracts!MediaEngine}\n * backed by the `audio-decode` package (pure JS/WASM codecs — no ffmpeg, no native bindings;\n * works in Node and browsers).\n *\n * @module @nhtio/adk/batteries/media/engines/audio_decode\n *\n * @remarks\n * Declares one convert capability: audio containers to the virtual `pcm` token (mp3 /\n * m4a-aac / ogg-vorbis / opus / flac / wav), downmixed to mono. The PCM output reports the\n * SOURCE sample rate in `meta.sampleRate` — the pipeline's transcribe step resamples to the\n * 16 kHz transcription engines expect. For exotic containers, compose an ffmpeg-backed\n * engine instead; the capability declaration is the seam.\n *\n * `audio-decode` is an optional peer dependency, lazily imported on first actual use.\n */\n\nimport { isError } from '@nhtio/adk/guards'\nimport { pcmToBytes, PCM_MIME } from '../contracts'\nimport { E_INVALID_MEDIA_PIPELINE_CONFIG } from '../exceptions'\nimport type { MediaEngine, ConvertRequest, ConvertResult } from '../contracts'\n\n/**\n * The decoded shapes audio-decode resolves to. Some codecs return an AudioBuffer-compatible\n * object (`numberOfChannels` + `getChannelData`); others (e.g. the wav path in Node) return a\n * plain `{ channelData: Float32Array[], sampleRate }` record. The engine normalizes both.\n */\
|
|
1
|
+
{"version":3,"file":"audio_decode.cjs","names":[],"sources":["../../../../src/batteries/media/engines/audio_decode.ts"],"sourcesContent":["/**\n * A cross-environment audio-decoding {@link @nhtio/adk/batteries/media/contracts!MediaEngine}\n * backed by the `audio-decode` package (pure JS/WASM codecs — no ffmpeg, no native bindings;\n * works in Node and browsers).\n *\n * @module @nhtio/adk/batteries/media/engines/audio_decode\n *\n * @remarks\n * Declares one convert capability: audio containers to the virtual `pcm` token (mp3 /\n * m4a-aac / ogg-vorbis / opus / flac / wav), downmixed to mono. The PCM output reports the\n * SOURCE sample rate in `meta.sampleRate` — the pipeline's transcribe step resamples to the\n * 16 kHz transcription engines expect. For exotic containers, compose an ffmpeg-backed\n * engine instead; the capability declaration is the seam.\n *\n * `audio-decode` is an optional peer dependency, lazily imported on first actual use.\n */\n\nimport { isError } from '@nhtio/adk/guards'\nimport { pcmToBytes, PCM_MIME } from '../contracts'\nimport { E_INVALID_MEDIA_PIPELINE_CONFIG } from '../exceptions'\nimport type { MediaEngine, ConvertRequest, ConvertResult } from '../contracts'\n\n/**\n * The decoded shapes audio-decode resolves to. Some codecs return an AudioBuffer-compatible\n * object (`numberOfChannels` + `getChannelData`); others (e.g. the wav path in Node) return a\n * plain `{ channelData: Float32Array[], sampleRate }` record. The engine normalizes both.\n */\nexport interface AudioBufferLike {\n /** Channel count when the AudioBuffer-compatible shape is returned. */\n numberOfChannels?: number\n /** Sample rate of the decoded audio, in Hz. Present on both shapes. */\n sampleRate: number\n /** Per-channel sample accessor on the AudioBuffer-compatible shape. */\n getChannelData?(channel: number): Float32Array\n /** Raw per-channel sample arrays on the plain-record shape. */\n channelData?: Float32Array[]\n}\n\n/** The decode function shape the `audio-decode` package exports. */\nexport type AudioDecodeFn = (bytes: Uint8Array | ArrayBuffer) => Promise<AudioBufferLike>\n\nconst channelsOf = (buffer: AudioBufferLike): Float32Array[] => {\n if (Array.isArray(buffer.channelData)) return buffer.channelData\n if (typeof buffer.getChannelData === 'function') {\n const count = buffer.numberOfChannels ?? 1\n return Array.from({ length: count }, (_, c) => buffer.getChannelData!(c))\n }\n throw new Error('audio-decode returned an unrecognized buffer shape')\n}\n\n/** Options for {@link audioDecodeEngine}. */\nexport interface AudioDecodeEngineOptions {\n /** Override the module resolution. Default: `import('audio-decode')`. */\n audioDecode?: () =>\n | AudioDecodeFn\n | { default: AudioDecodeFn }\n | Promise<AudioDecodeFn | { default: AudioDecodeFn }>\n}\n\n/**\n * Construct the audio-decode-backed engine.\n *\n * @param options - Optional module resolver override.\n * @returns The engine.\n */\nexport const audioDecodeEngine = (options: AudioDecodeEngineOptions = {}): MediaEngine => {\n let fnPromise: Promise<AudioDecodeFn> | undefined\n const getDecode = (): Promise<AudioDecodeFn> => {\n fnPromise ??= Promise.resolve(\n options.audioDecode ? options.audioDecode() : import('audio-decode')\n )\n .then((mod) => {\n const fn = typeof mod === 'function' ? mod : (mod as { default: AudioDecodeFn }).default\n if (typeof fn !== 'function') {\n throw new Error('audio-decode did not resolve to a decode function')\n }\n return fn\n })\n .catch((err) => {\n const detail = isError(err) ? err.message : String(err)\n throw new E_INVALID_MEDIA_PIPELINE_CONFIG([\n `the audio-decode engine could not load its peer dependency \"audio-decode\": ${detail} — install it (pnpm add audio-decode)`,\n ])\n })\n return fnPromise\n }\n\n const convert = async (request: ConvertRequest): Promise<ConvertResult> => {\n const decode = await getDecode()\n const buffer = await decode(request.bytes)\n const channels = channelsOf(buffer)\n let pcm: Float32Array\n if (channels.length <= 1) {\n pcm = channels[0]\n } else {\n // Downmix to mono by averaging channels.\n const length = channels[0].length\n const mono = new Float32Array(length)\n for (const data of channels) {\n for (let i = 0; i < length; i++) mono[i] += data[i] / channels.length\n }\n pcm = mono\n }\n return {\n outputs: [\n { bytes: pcmToBytes(pcm), mimeType: PCM_MIME, meta: { sampleRate: buffer.sampleRate } },\n ],\n }\n }\n\n return {\n id: 'audio-decode',\n converts: [\n {\n from: [\n 'audio/mpeg',\n 'audio/mp3',\n 'audio/mp4',\n 'audio/aac',\n 'audio/x-m4a',\n 'audio/ogg',\n 'audio/opus',\n 'audio/flac',\n 'audio/x-flac',\n 'audio/wav',\n 'audio/x-wav',\n 'audio/wave',\n ],\n to: ['pcm'],\n convert,\n },\n ],\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAyCA,IAAM,cAAc,WAA4C;CAC9D,IAAI,MAAM,QAAQ,OAAO,WAAW,GAAG,OAAO,OAAO;CACrD,IAAI,OAAO,OAAO,mBAAmB,YAAY;EAC/C,MAAM,QAAQ,OAAO,oBAAoB;EACzC,OAAO,MAAM,KAAK,EAAE,QAAQ,MAAM,IAAI,GAAG,MAAM,OAAO,eAAgB,CAAC,CAAC;CAC1E;CACA,MAAM,IAAI,MAAM,oDAAoD;AACtE;;;;;;;AAiBA,IAAa,qBAAqB,UAAoC,CAAC,MAAmB;CACxF,IAAI;CACJ,MAAM,kBAA0C;EAC9C,cAAc,QAAQ,QACpB,QAAQ,cAAc,QAAQ,YAAY,IAAI,OAAO,eACvD,EACG,MAAM,QAAQ;GACb,MAAM,KAAK,OAAO,QAAQ,aAAa,MAAO,IAAmC;GACjF,IAAI,OAAO,OAAO,YAChB,MAAM,IAAI,MAAM,mDAAmD;GAErE,OAAO;EACT,CAAC,EACA,OAAO,QAAQ;GAEd,MAAM,IAAI,mBAAA,gCAAgC,CACxC,8EAFa,sBAAA,QAAQ,GAAG,IAAI,IAAI,UAAU,OAAO,GAAG,EAEiC,sCACvF,CAAC;EACH,CAAC;EACH,OAAO;CACT;CAEA,MAAM,UAAU,OAAO,YAAoD;EAEzE,MAAM,SAAS,OAAM,MADA,UAAU,GACH,QAAQ,KAAK;EACzC,MAAM,WAAW,WAAW,MAAM;EAClC,IAAI;EACJ,IAAI,SAAS,UAAU,GACrB,MAAM,SAAS;OACV;GAEL,MAAM,SAAS,SAAS,GAAG;GAC3B,MAAM,OAAO,IAAI,aAAa,MAAM;GACpC,KAAK,MAAM,QAAQ,UACjB,KAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,KAAK,KAAK,MAAM,KAAK,KAAK,SAAS;GAEjE,MAAM;EACR;EACA,OAAO,EACL,SAAS,CACP;GAAE,OAAO,kCAAA,WAAW,GAAG;GAAG,UAAU,kCAAA;GAAU,MAAM,EAAE,YAAY,OAAO,WAAW;EAAE,CACxF,EACF;CACF;CAEA,OAAO;EACL,IAAI;EACJ,UAAU,CACR;GACE,MAAM;IACJ;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;GACF;GACA,IAAI,CAAC,KAAK;GACV;EACF,CACF;CACF;AACF"}
|
|
@@ -20,13 +20,18 @@ import type { MediaEngine } from "../contracts";
|
|
|
20
20
|
* object (`numberOfChannels` + `getChannelData`); others (e.g. the wav path in Node) return a
|
|
21
21
|
* plain `{ channelData: Float32Array[], sampleRate }` record. The engine normalizes both.
|
|
22
22
|
*/
|
|
23
|
-
interface AudioBufferLike {
|
|
23
|
+
export interface AudioBufferLike {
|
|
24
|
+
/** Channel count when the AudioBuffer-compatible shape is returned. */
|
|
24
25
|
numberOfChannels?: number;
|
|
26
|
+
/** Sample rate of the decoded audio, in Hz. Present on both shapes. */
|
|
25
27
|
sampleRate: number;
|
|
28
|
+
/** Per-channel sample accessor on the AudioBuffer-compatible shape. */
|
|
26
29
|
getChannelData?(channel: number): Float32Array;
|
|
30
|
+
/** Raw per-channel sample arrays on the plain-record shape. */
|
|
27
31
|
channelData?: Float32Array[];
|
|
28
32
|
}
|
|
29
|
-
|
|
33
|
+
/** The decode function shape the `audio-decode` package exports. */
|
|
34
|
+
export type AudioDecodeFn = (bytes: Uint8Array | ArrayBuffer) => Promise<AudioBufferLike>;
|
|
30
35
|
/** Options for {@link audioDecodeEngine}. */
|
|
31
36
|
export interface AudioDecodeEngineOptions {
|
|
32
37
|
/** Override the module resolution. Default: `import('audio-decode')`. */
|
|
@@ -43,4 +48,3 @@ export interface AudioDecodeEngineOptions {
|
|
|
43
48
|
* @returns The engine.
|
|
44
49
|
*/
|
|
45
50
|
export declare const audioDecodeEngine: (options?: AudioDecodeEngineOptions) => MediaEngine;
|
|
46
|
-
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audio_decode.mjs","names":[],"sources":["../../../../src/batteries/media/engines/audio_decode.ts"],"sourcesContent":["/**\n * A cross-environment audio-decoding {@link @nhtio/adk/batteries/media/contracts!MediaEngine}\n * backed by the `audio-decode` package (pure JS/WASM codecs — no ffmpeg, no native bindings;\n * works in Node and browsers).\n *\n * @module @nhtio/adk/batteries/media/engines/audio_decode\n *\n * @remarks\n * Declares one convert capability: audio containers to the virtual `pcm` token (mp3 /\n * m4a-aac / ogg-vorbis / opus / flac / wav), downmixed to mono. The PCM output reports the\n * SOURCE sample rate in `meta.sampleRate` — the pipeline's transcribe step resamples to the\n * 16 kHz transcription engines expect. For exotic containers, compose an ffmpeg-backed\n * engine instead; the capability declaration is the seam.\n *\n * `audio-decode` is an optional peer dependency, lazily imported on first actual use.\n */\n\nimport { isError } from '@nhtio/adk/guards'\nimport { pcmToBytes, PCM_MIME } from '../contracts'\nimport { E_INVALID_MEDIA_PIPELINE_CONFIG } from '../exceptions'\nimport type { MediaEngine, ConvertRequest, ConvertResult } from '../contracts'\n\n/**\n * The decoded shapes audio-decode resolves to. Some codecs return an AudioBuffer-compatible\n * object (`numberOfChannels` + `getChannelData`); others (e.g. the wav path in Node) return a\n * plain `{ channelData: Float32Array[], sampleRate }` record. The engine normalizes both.\n */\
|
|
1
|
+
{"version":3,"file":"audio_decode.mjs","names":[],"sources":["../../../../src/batteries/media/engines/audio_decode.ts"],"sourcesContent":["/**\n * A cross-environment audio-decoding {@link @nhtio/adk/batteries/media/contracts!MediaEngine}\n * backed by the `audio-decode` package (pure JS/WASM codecs — no ffmpeg, no native bindings;\n * works in Node and browsers).\n *\n * @module @nhtio/adk/batteries/media/engines/audio_decode\n *\n * @remarks\n * Declares one convert capability: audio containers to the virtual `pcm` token (mp3 /\n * m4a-aac / ogg-vorbis / opus / flac / wav), downmixed to mono. The PCM output reports the\n * SOURCE sample rate in `meta.sampleRate` — the pipeline's transcribe step resamples to the\n * 16 kHz transcription engines expect. For exotic containers, compose an ffmpeg-backed\n * engine instead; the capability declaration is the seam.\n *\n * `audio-decode` is an optional peer dependency, lazily imported on first actual use.\n */\n\nimport { isError } from '@nhtio/adk/guards'\nimport { pcmToBytes, PCM_MIME } from '../contracts'\nimport { E_INVALID_MEDIA_PIPELINE_CONFIG } from '../exceptions'\nimport type { MediaEngine, ConvertRequest, ConvertResult } from '../contracts'\n\n/**\n * The decoded shapes audio-decode resolves to. Some codecs return an AudioBuffer-compatible\n * object (`numberOfChannels` + `getChannelData`); others (e.g. the wav path in Node) return a\n * plain `{ channelData: Float32Array[], sampleRate }` record. The engine normalizes both.\n */\nexport interface AudioBufferLike {\n /** Channel count when the AudioBuffer-compatible shape is returned. */\n numberOfChannels?: number\n /** Sample rate of the decoded audio, in Hz. Present on both shapes. */\n sampleRate: number\n /** Per-channel sample accessor on the AudioBuffer-compatible shape. */\n getChannelData?(channel: number): Float32Array\n /** Raw per-channel sample arrays on the plain-record shape. */\n channelData?: Float32Array[]\n}\n\n/** The decode function shape the `audio-decode` package exports. */\nexport type AudioDecodeFn = (bytes: Uint8Array | ArrayBuffer) => Promise<AudioBufferLike>\n\nconst channelsOf = (buffer: AudioBufferLike): Float32Array[] => {\n if (Array.isArray(buffer.channelData)) return buffer.channelData\n if (typeof buffer.getChannelData === 'function') {\n const count = buffer.numberOfChannels ?? 1\n return Array.from({ length: count }, (_, c) => buffer.getChannelData!(c))\n }\n throw new Error('audio-decode returned an unrecognized buffer shape')\n}\n\n/** Options for {@link audioDecodeEngine}. */\nexport interface AudioDecodeEngineOptions {\n /** Override the module resolution. Default: `import('audio-decode')`. */\n audioDecode?: () =>\n | AudioDecodeFn\n | { default: AudioDecodeFn }\n | Promise<AudioDecodeFn | { default: AudioDecodeFn }>\n}\n\n/**\n * Construct the audio-decode-backed engine.\n *\n * @param options - Optional module resolver override.\n * @returns The engine.\n */\nexport const audioDecodeEngine = (options: AudioDecodeEngineOptions = {}): MediaEngine => {\n let fnPromise: Promise<AudioDecodeFn> | undefined\n const getDecode = (): Promise<AudioDecodeFn> => {\n fnPromise ??= Promise.resolve(\n options.audioDecode ? options.audioDecode() : import('audio-decode')\n )\n .then((mod) => {\n const fn = typeof mod === 'function' ? mod : (mod as { default: AudioDecodeFn }).default\n if (typeof fn !== 'function') {\n throw new Error('audio-decode did not resolve to a decode function')\n }\n return fn\n })\n .catch((err) => {\n const detail = isError(err) ? err.message : String(err)\n throw new E_INVALID_MEDIA_PIPELINE_CONFIG([\n `the audio-decode engine could not load its peer dependency \"audio-decode\": ${detail} — install it (pnpm add audio-decode)`,\n ])\n })\n return fnPromise\n }\n\n const convert = async (request: ConvertRequest): Promise<ConvertResult> => {\n const decode = await getDecode()\n const buffer = await decode(request.bytes)\n const channels = channelsOf(buffer)\n let pcm: Float32Array\n if (channels.length <= 1) {\n pcm = channels[0]\n } else {\n // Downmix to mono by averaging channels.\n const length = channels[0].length\n const mono = new Float32Array(length)\n for (const data of channels) {\n for (let i = 0; i < length; i++) mono[i] += data[i] / channels.length\n }\n pcm = mono\n }\n return {\n outputs: [\n { bytes: pcmToBytes(pcm), mimeType: PCM_MIME, meta: { sampleRate: buffer.sampleRate } },\n ],\n }\n }\n\n return {\n id: 'audio-decode',\n converts: [\n {\n from: [\n 'audio/mpeg',\n 'audio/mp3',\n 'audio/mp4',\n 'audio/aac',\n 'audio/x-m4a',\n 'audio/ogg',\n 'audio/opus',\n 'audio/flac',\n 'audio/x-flac',\n 'audio/wav',\n 'audio/x-wav',\n 'audio/wave',\n ],\n to: ['pcm'],\n convert,\n },\n ],\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAyCA,IAAM,cAAc,WAA4C;CAC9D,IAAI,MAAM,QAAQ,OAAO,WAAW,GAAG,OAAO,OAAO;CACrD,IAAI,OAAO,OAAO,mBAAmB,YAAY;EAC/C,MAAM,QAAQ,OAAO,oBAAoB;EACzC,OAAO,MAAM,KAAK,EAAE,QAAQ,MAAM,IAAI,GAAG,MAAM,OAAO,eAAgB,CAAC,CAAC;CAC1E;CACA,MAAM,IAAI,MAAM,oDAAoD;AACtE;;;;;;;AAiBA,IAAa,qBAAqB,UAAoC,CAAC,MAAmB;CACxF,IAAI;CACJ,MAAM,kBAA0C;EAC9C,cAAc,QAAQ,QACpB,QAAQ,cAAc,QAAQ,YAAY,IAAI,OAAO,eACvD,EACG,MAAM,QAAQ;GACb,MAAM,KAAK,OAAO,QAAQ,aAAa,MAAO,IAAmC;GACjF,IAAI,OAAO,OAAO,YAChB,MAAM,IAAI,MAAM,mDAAmD;GAErE,OAAO;EACT,CAAC,EACA,OAAO,QAAQ;GAEd,MAAM,IAAI,gCAAgC,CACxC,8EAFa,QAAQ,GAAG,IAAI,IAAI,UAAU,OAAO,GAAG,EAEiC,sCACvF,CAAC;EACH,CAAC;EACH,OAAO;CACT;CAEA,MAAM,UAAU,OAAO,YAAoD;EAEzE,MAAM,SAAS,OAAM,MADA,UAAU,GACH,QAAQ,KAAK;EACzC,MAAM,WAAW,WAAW,MAAM;EAClC,IAAI;EACJ,IAAI,SAAS,UAAU,GACrB,MAAM,SAAS;OACV;GAEL,MAAM,SAAS,SAAS,GAAG;GAC3B,MAAM,OAAO,IAAI,aAAa,MAAM;GACpC,KAAK,MAAM,QAAQ,UACjB,KAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,KAAK,KAAK,MAAM,KAAK,KAAK,SAAS;GAEjE,MAAM;EACR;EACA,OAAO,EACL,SAAS,CACP;GAAE,OAAO,WAAW,GAAG;GAAG,UAAU;GAAU,MAAM,EAAE,YAAY,OAAO,WAAW;EAAE,CACxF,EACF;CACF;CAEA,OAAO;EACL,IAAI;EACJ,UAAU,CACR;GACE,MAAM;IACJ;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;GACF;GACA,IAAI,CAAC,KAAK;GACV;EACF,CACF;CACF;AACF"}
|
|
@@ -46,7 +46,7 @@ import type { EngineSelectionMiddlewareFn } from "./registry";
|
|
|
46
46
|
import type { MediaEngine, EngineResolver } from "./contracts";
|
|
47
47
|
import type { StepPayload, PlanResult, MediaStepMiddlewareFn } from "./runtime";
|
|
48
48
|
export { MediaChain } from "./builder";
|
|
49
|
-
export type { ChainInput, MediaChainRef, CellUpdate, ResizeOptions, SheetNamespace, SlidesNamespace, ImageNamespace, AudioNamespace, } from "./builder";
|
|
49
|
+
export type { ChainExecutor, ChainInput, MediaChainRef, CellUpdate, ResizeOptions, SheetNamespace, SlidesNamespace, ImageNamespace, AudioNamespace, } from "./builder";
|
|
50
50
|
export { toPipe, toOps, fromOps, isMediaRef, isRegExpRef } from "./plan";
|
|
51
51
|
export type { MediaPlan, MediaStep, MediaOp, MediaArgValue, MediaArgScalar, MediaArgJson, MediaRef, RegExpRef, SourceSpan, } from "./plan";
|
|
52
52
|
export { parsePipeRaw, lowerSegments } from "./pipe";
|
|
@@ -68,7 +68,7 @@ export type MediaRefResolver = (id: string) => StepPayload | Promise<StepPayload
|
|
|
68
68
|
export interface RunOptions {
|
|
69
69
|
/** Abort signal threaded into steps and engines. */
|
|
70
70
|
signal?: AbortSignal;
|
|
71
|
-
/** Per-run media-ref resolution, overriding the construction-time resolver (the agent forge uses this to resolve
|
|
71
|
+
/** Per-run media-ref resolution, overriding the construction-time resolver (the agent forge uses this to resolve `@id` refs against the current turn). */
|
|
72
72
|
resolveRef?: MediaRefResolver;
|
|
73
73
|
}
|
|
74
74
|
/** Configuration for {@link createMediaPipeline}. */
|
package/batteries/media/lint.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lint.cjs","names":[],"sources":["../../../src/batteries/media/lint.ts"],"sourcesContent":["/**\n * @module @nhtio/adk/batteries/media/lint\n *\n * ESLint rules for consumers of the media pipeline battery: machine-checkable enforcement of\n * the engine-composition contracts implementors are most likely to get wrong.\n *\n * @remarks\n * Battery-scoped rules ship with the battery, not with the core `@nhtio/adk/eslint` plugin —\n * they only matter to deployments that compose this battery, and they version with the\n * battery's own API. The rules are report-only (no autofix); carve out a deliberate exception\n * with an inline `// eslint-disable-next-line adk-media/{rule} -- {reason}` comment.\n *\n * `@typescript-eslint/utils` and `eslint` are OPTIONAL peer dependencies of `@nhtio/adk` —\n * installed only by consumers who lint with this plugin. The battery never imports this\n * module at runtime.\n *\n * @example Flat config\n * ```ts\n * import adkMedia from '@nhtio/adk/batteries/media/lint'\n *\n * export default [\n * { plugins: { 'adk-media': adkMedia.plugin }, rules: adkMedia.configs.recommended.rules },\n * ]\n * ```\n */\n\nimport { ESLintUtils } from '@typescript-eslint/utils'\nimport type { TSESTree } from '@typescript-eslint/utils'\nimport type { FlatConfig } from '@typescript-eslint/utils/ts-eslint'\n\nconst createRule = ESLintUtils.RuleCreator(\n (name) => `https://adk.nht.io/batteries/media/lint#${name}`\n)\n\n// ── prefer-engine-resolver ───────────────────────────────────────────────────\n\nconst ENGINE_SUBPATH = /^@nhtio\\/adk\\/batteries\\/media\\/engines\\//\n\n/**\n * Flags a static value import of a bundled engine subpath. The documented canonical supply\n * form is the dynamic-import resolver, which keeps the engine wrapper module out of every\n * bundle that merely might use it. Type-only imports are fine.\n */\nconst preferEngineResolver = createRule({\n name: 'prefer-engine-resolver',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Supply bundled media engines as dynamic-import resolvers, not static imports — a static import puts the engine wrapper in every bundle, even when the engine is conditional.',\n },\n schema: [],\n messages: {\n preferResolver:\n \"Import bundled engines lazily: supply `() => import('{{source}}').then((m) => m.{{hint}}(…))` in the engines array instead of a static import. Static imports put the engine wrapper module in every bundle. Opt out with an eslint-disable-next-line adk-media/prefer-engine-resolver comment + reason.\",\n },\n },\n defaultOptions: [],\n create(context) {\n return {\n ImportDeclaration(node: TSESTree.ImportDeclaration) {\n if (typeof node.source.value !== 'string') return\n if (!ENGINE_SUBPATH.test(node.source.value)) return\n if (node.importKind === 'type') return\n const valueSpecifiers = node.specifiers.filter(\n (s) => !(s.type === 'ImportSpecifier' && s.importKind === 'type')\n )\n if (valueSpecifiers.length === 0) return\n const first = valueSpecifiers[0]\n const hint =\n first.type === 'ImportSpecifier' && first.imported.type === 'Identifier'\n ? first.imported.name\n : 'engineFactory'\n context.report({\n node,\n messageId: 'preferResolver',\n data: { source: node.source.value, hint },\n })\n },\n }\n },\n})\n\n// ── no-shadowed-engine ───────────────────────────────────────────────────────\n\n/** A statically-derived capability summary for one engines-array element. */\ninterface EngineSummary {\n /** Display label for the diagnostic. */\n label: string\n /** Mutate groups: input patterns, ops, encodes. */\n mutates: Array<{ over: string[]; ops: string[]; encodes: string[] }>\n /** Convert groups: input patterns, target tokens. */\n converts: Array<{ from: string[]; to: string[] }>\n}\n\n/**\n * Known declarations of the bundled engine factories, for shadow analysis. An ESLint rule\n * cannot execute the factories it lints, so this table mirrors their declarations by hand —\n * and is pinned against the live factories by a drift test (`lint_drift.node.spec.ts`).\n * Exported for that test only.\n */\nexport const BUNDLED_SUMMARIES: Record<string, EngineSummary> = {\n jimpEngine: {\n label: 'jimpEngine',\n mutates: [\n {\n over: ['image/png', 'image/jpeg', 'image/bmp', 'image/gif', 'image/tiff'],\n ops: ['resize', 'rotate', 'flip', 'strip_metadata'],\n encodes: ['png', 'jpg', 'jpeg', 'bmp', 'gif', 'tiff'],\n },\n ],\n converts: [],\n },\n sharpEngine: {\n label: 'sharpEngine',\n mutates: [\n {\n over: ['image/*'],\n ops: ['resize', 'rotate', 'flip', 'strip_metadata'],\n encodes: ['png', 'jpg', 'jpeg', 'webp', 'tiff', 'avif', 'gif'],\n },\n ],\n converts: [],\n },\n}\n\nconst literalStrings = (node: TSESTree.Node | undefined): string[] | undefined => {\n if (!node || node.type !== 'ArrayExpression') return undefined\n const out: string[] = []\n for (const el of node.elements) {\n if (!el || el.type !== 'Literal' || typeof el.value !== 'string') return undefined\n out.push(el.value)\n }\n return out\n}\n\nconst propOf = (obj: TSESTree.ObjectExpression, name: string): TSESTree.Node | undefined =>\n obj.properties.find(\n (p): p is TSESTree.Property =>\n p.type === 'Property' && !p.computed && p.key.type === 'Identifier' && p.key.name === name\n )?.value\n\n/** Find the bundled factory name referenced by an element (direct call or resolver arrow). */\nconst bundledFactoryName = (node: TSESTree.Node): string | undefined => {\n if (\n node.type === 'CallExpression' &&\n node.callee.type === 'Identifier' &&\n node.callee.name in BUNDLED_SUMMARIES\n ) {\n return node.callee.name\n }\n // Resolver forms: () => …; scan the body for m.jimpEngine(…) / jimpEngine(…).\n if (node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression') {\n let found: string | undefined\n const visit = (n: TSESTree.Node): void => {\n if (found) return\n if (n.type === 'CallExpression') {\n if (n.callee.type === 'Identifier' && n.callee.name in BUNDLED_SUMMARIES) {\n found = n.callee.name\n } else if (\n n.callee.type === 'MemberExpression' &&\n n.callee.property.type === 'Identifier' &&\n n.callee.property.name in BUNDLED_SUMMARIES\n ) {\n found = n.callee.property.name\n }\n }\n for (const key of Object.keys(n)) {\n if (key === 'parent') continue\n const value = (n as unknown as Record<string, unknown>)[key]\n const children = Array.isArray(value) ? value : [value]\n for (const child of children) {\n if (!child || typeof child !== 'object') continue\n const childNode = child as TSESTree.Node\n if (typeof childNode.type !== 'string') continue\n visit(childNode)\n }\n }\n }\n visit(node.body)\n return found\n }\n return undefined\n}\n\n/** Derive a capability summary from an element, or undefined when not statically known. */\nconst summarize = (node: TSESTree.Node): EngineSummary | undefined => {\n const factory = bundledFactoryName(node)\n if (factory) return BUNDLED_SUMMARIES[factory]\n if (node.type !== 'ObjectExpression') return undefined\n const idNode = propOf(node, 'id')\n const label =\n idNode?.type === 'Literal' && typeof idNode.value === 'string' ? idNode.value : 'engine'\n const summary: EngineSummary = { label, mutates: [], converts: [] }\n const mutates = propOf(node, 'mutates')\n if (mutates) {\n if (mutates.type !== 'ArrayExpression') return undefined\n for (const el of mutates.elements) {\n if (!el || el.type !== 'ObjectExpression') return undefined\n const over = literalStrings(propOf(el, 'over'))\n const ops = literalStrings(propOf(el, 'ops'))\n const encodes = literalStrings(propOf(el, 'encodes'))\n if (!over || !ops || !encodes) return undefined\n summary.mutates.push({ over, ops, encodes })\n }\n }\n const converts = propOf(node, 'converts')\n if (converts) {\n if (converts.type !== 'ArrayExpression') return undefined\n for (const el of converts.elements) {\n if (!el || el.type !== 'ObjectExpression') return undefined\n const from = literalStrings(propOf(el, 'from'))\n const to = literalStrings(propOf(el, 'to'))\n if (!from || !to) return undefined\n summary.converts.push({ from, to })\n }\n }\n if (summary.mutates.length + summary.converts.length === 0) return undefined\n return summary\n}\n\nconst patternCovers = (broad: string, narrow: string): boolean => {\n if (broad === narrow) return true\n if (broad.endsWith('/*')) return narrow.startsWith(broad.slice(0, -1))\n return false\n}\n\nconst patternsCover = (broad: string[], narrow: string[]): boolean =>\n narrow.every((n) => broad.some((b) => patternCovers(b, n)))\n\nconst subset = (sup: string[], sub: string[]): boolean => sub.every((s) => sup.includes(s))\n\n/** `true` when EVERY capability of `later` is subsumed by some capability of `earlier`. */\nconst shadows = (earlier: EngineSummary, later: EngineSummary): boolean => {\n if (later.mutates.length + later.converts.length === 0) return false\n const mutatesCovered = later.mutates.every((lm) =>\n earlier.mutates.some(\n (em) =>\n patternsCover(em.over, lm.over) && subset(em.ops, lm.ops) && subset(em.encodes, lm.encodes)\n )\n )\n const convertsCovered = later.converts.every((lc) =>\n earlier.converts.some((ec) => patternsCover(ec.from, lc.from) && subset(ec.to, lc.to))\n )\n return mutatesCovered && convertsCovered\n}\n\n/**\n * Flags an engines-array element whose statically-known capabilities are entirely covered by\n * an EARLIER element — dispatch is first-capable-wins, so the later engine is dead code.\n */\nconst noShadowedEngine = createRule({\n name: 'no-shadowed-engine',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'An engine whose declared capabilities are a subset of an earlier engine in the array can never be selected — first-capable-wins dispatch makes it dead code. Reorder (narrow before broad) or remove it.',\n },\n schema: [],\n messages: {\n shadowed:\n 'Engine \"{{later}}\" can never be selected: \"{{earlier}}\" appears earlier in the array and declares a superset of its capabilities (dispatch is first-capable-wins). Put the narrower engine first, or remove it. Opt out with an eslint-disable-next-line adk-media/no-shadowed-engine comment + reason.',\n },\n },\n defaultOptions: [],\n create(context) {\n return {\n CallExpression(node: TSESTree.CallExpression) {\n if (node.callee.type !== 'Identifier' || node.callee.name !== 'createMediaPipeline') {\n return\n }\n const config = node.arguments[0]\n if (!config || config.type !== 'ObjectExpression') return\n const engines = propOf(config, 'engines')\n if (!engines || engines.type !== 'ArrayExpression') return\n const summaries = engines.elements.map((el) => (el ? summarize(el) : undefined))\n for (let i = 1; i < summaries.length; i++) {\n const later = summaries[i]\n if (!later) continue\n for (let j = 0; j < i; j++) {\n const earlier = summaries[j]\n if (!earlier) continue\n if (shadows(earlier, later)) {\n context.report({\n node: engines.elements[i]!,\n messageId: 'shadowed',\n data: { earlier: earlier.label, later: later.label },\n })\n break\n }\n }\n }\n },\n }\n },\n})\n\n// ── augment-contracts-module ─────────────────────────────────────────────────\n\nconst CONTRACTS_SUBPATH = '@nhtio/adk/batteries/media/contracts'\n\n/**\n * Flags a `ConvertOptions` declaration-merging block whose module specifier is not the\n * contracts subpath. Augmenting the barrel (or any other module) silently does nothing —\n * the keys never merge, and the typo costs hours.\n */\nconst augmentContractsModule = createRule({\n name: 'augment-contracts-module',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'ConvertOptions augmentation must target the contracts subpath — declaration merging against any other module specifier silently fails to merge.',\n },\n schema: [],\n messages: {\n wrongModule:\n 'This ConvertOptions augmentation targets \"{{actual}}\", so it will silently never merge. Declaration merging must target the module that declares the interface: declare module \\'{{expected}}\\'.',\n },\n },\n defaultOptions: [],\n create(context) {\n return {\n TSModuleDeclaration(node: TSESTree.TSModuleDeclaration) {\n if (node.global) return\n if (node.id.type !== 'Literal' || typeof node.id.value !== 'string') return\n const specifier = node.id.value\n if (specifier === CONTRACTS_SUBPATH || specifier.endsWith('/media/contracts')) return\n // Only flag specifiers that are plausibly aimed at this battery.\n if (!specifier.includes('media') && !specifier.includes('@nhtio/adk')) return\n const body = node.body\n if (!body || body.type !== 'TSModuleBlock') return\n const declaresConvertOptions = body.body.some(\n (stmt) => stmt.type === 'TSInterfaceDeclaration' && stmt.id.name === 'ConvertOptions'\n )\n if (!declaresConvertOptions) return\n context.report({\n node: node.id,\n messageId: 'wrongModule',\n data: { actual: specifier, expected: CONTRACTS_SUBPATH },\n })\n },\n }\n },\n})\n\n// ── plugin assembly ──────────────────────────────────────────────────────────\n\n/**\n * Map of rule id (without the `adk-media/` plugin prefix) to its rule object. Registered on\n * the plugin as `rules`, so configs reference them as `adk-media/{id}`.\n */\nexport const rules = {\n 'prefer-engine-resolver': preferEngineResolver,\n 'no-shadowed-engine': noShadowedEngine,\n 'augment-contracts-module': augmentContractsModule,\n} satisfies FlatConfig.Plugin['rules']\n\n/**\n * The media battery's ESLint plugin object. Register under the `adk-media` namespace:\n * `plugins: { 'adk-media': plugin }`.\n */\nexport const plugin: FlatConfig.Plugin = {\n meta: { name: '@nhtio/adk/batteries/media/lint', version: __VERSION__ },\n rules,\n}\n\nconst recommendedRules: NonNullable<FlatConfig.Config['rules']> = Object.fromEntries(\n Object.keys(rules).map((id) => [`adk-media/${id}`, 'error'])\n)\n\n/**\n * Named config presets. `recommended` enables every rule at `error` and registers the plugin\n * under the `adk-media` namespace — spread it into a flat config to adopt the full set.\n */\nexport const configs = {\n recommended: {\n name: '@nhtio/adk/batteries/media/lint/recommended',\n plugins: { 'adk-media': plugin },\n rules: recommendedRules,\n } satisfies FlatConfig.Config,\n}\n\n/**\n * Default export bundles the plugin and its config presets, mirroring the shape ESLint flat\n * configs expect from a plugin module.\n */\nexport default { plugin, rules, configs }\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lint.cjs","names":[],"sources":["../../../src/batteries/media/lint.ts"],"sourcesContent":["/**\n * @module @nhtio/adk/batteries/media/lint\n *\n * ESLint rules for consumers of the media pipeline battery: machine-checkable enforcement of\n * the engine-composition contracts implementors are most likely to get wrong.\n *\n * @remarks\n * Battery-scoped rules ship with the battery, not with the core `@nhtio/adk/eslint` plugin —\n * they only matter to deployments that compose this battery, and they version with the\n * battery's own API. The rules are report-only (no autofix); carve out a deliberate exception\n * with an inline `// eslint-disable-next-line adk-media/{rule} -- {reason}` comment.\n *\n * `@typescript-eslint/utils` and `eslint` are OPTIONAL peer dependencies of `@nhtio/adk` —\n * installed only by consumers who lint with this plugin. The battery never imports this\n * module at runtime.\n *\n * @example Flat config\n * ```ts\n * import adkMedia from '@nhtio/adk/batteries/media/lint'\n *\n * export default [\n * { plugins: { 'adk-media': adkMedia.plugin }, rules: adkMedia.configs.recommended.rules },\n * ]\n * ```\n */\n\nimport { ESLintUtils } from '@typescript-eslint/utils'\nimport type { TSESTree } from '@typescript-eslint/utils'\nimport type { FlatConfig } from '@typescript-eslint/utils/ts-eslint'\n\nconst createRule = ESLintUtils.RuleCreator(\n (name) => `https://adk.nht.io/batteries/media/lint#${name}`\n)\n\n// ── prefer-engine-resolver ───────────────────────────────────────────────────\n\nconst ENGINE_SUBPATH = /^@nhtio\\/adk\\/batteries\\/media\\/engines\\//\n\n/**\n * Flags a static value import of a bundled engine subpath. The documented canonical supply\n * form is the dynamic-import resolver, which keeps the engine wrapper module out of every\n * bundle that merely might use it. Type-only imports are fine.\n */\nconst preferEngineResolver = createRule({\n name: 'prefer-engine-resolver',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Supply bundled media engines as dynamic-import resolvers, not static imports — a static import puts the engine wrapper in every bundle, even when the engine is conditional.',\n },\n schema: [],\n messages: {\n preferResolver:\n \"Import bundled engines lazily: supply `() => import('{{source}}').then((m) => m.{{hint}}(…))` in the engines array instead of a static import. Static imports put the engine wrapper module in every bundle. Opt out with an eslint-disable-next-line adk-media/prefer-engine-resolver comment + reason.\",\n },\n },\n defaultOptions: [],\n create(context) {\n return {\n ImportDeclaration(node: TSESTree.ImportDeclaration) {\n if (typeof node.source.value !== 'string') return\n if (!ENGINE_SUBPATH.test(node.source.value)) return\n if (node.importKind === 'type') return\n const valueSpecifiers = node.specifiers.filter(\n (s) => !(s.type === 'ImportSpecifier' && s.importKind === 'type')\n )\n if (valueSpecifiers.length === 0) return\n const first = valueSpecifiers[0]\n const hint =\n first.type === 'ImportSpecifier' && first.imported.type === 'Identifier'\n ? first.imported.name\n : 'engineFactory'\n context.report({\n node,\n messageId: 'preferResolver',\n data: { source: node.source.value, hint },\n })\n },\n }\n },\n})\n\n// ── no-shadowed-engine ───────────────────────────────────────────────────────\n\n/** A statically-derived capability summary for one engines-array element. */\nexport interface EngineSummary {\n /** Display label for the diagnostic. */\n label: string\n /** Mutate groups: input patterns, ops, encodes. */\n mutates: Array<{ over: string[]; ops: string[]; encodes: string[] }>\n /** Convert groups: input patterns, target tokens. */\n converts: Array<{ from: string[]; to: string[] }>\n}\n\n/**\n * Known declarations of the bundled engine factories, for shadow analysis. An ESLint rule\n * cannot execute the factories it lints, so this table mirrors their declarations by hand —\n * and is pinned against the live factories by a drift test (`lint_drift.node.spec.ts`).\n * Exported for that test only.\n */\nexport const BUNDLED_SUMMARIES: Record<string, EngineSummary> = {\n jimpEngine: {\n label: 'jimpEngine',\n mutates: [\n {\n over: ['image/png', 'image/jpeg', 'image/bmp', 'image/gif', 'image/tiff'],\n ops: ['resize', 'rotate', 'flip', 'strip_metadata'],\n encodes: ['png', 'jpg', 'jpeg', 'bmp', 'gif', 'tiff'],\n },\n ],\n converts: [],\n },\n sharpEngine: {\n label: 'sharpEngine',\n mutates: [\n {\n over: ['image/*'],\n ops: ['resize', 'rotate', 'flip', 'strip_metadata'],\n encodes: ['png', 'jpg', 'jpeg', 'webp', 'tiff', 'avif', 'gif'],\n },\n ],\n converts: [],\n },\n}\n\nconst literalStrings = (node: TSESTree.Node | undefined): string[] | undefined => {\n if (!node || node.type !== 'ArrayExpression') return undefined\n const out: string[] = []\n for (const el of node.elements) {\n if (!el || el.type !== 'Literal' || typeof el.value !== 'string') return undefined\n out.push(el.value)\n }\n return out\n}\n\nconst propOf = (obj: TSESTree.ObjectExpression, name: string): TSESTree.Node | undefined =>\n obj.properties.find(\n (p): p is TSESTree.Property =>\n p.type === 'Property' && !p.computed && p.key.type === 'Identifier' && p.key.name === name\n )?.value\n\n/** Find the bundled factory name referenced by an element (direct call or resolver arrow). */\nconst bundledFactoryName = (node: TSESTree.Node): string | undefined => {\n if (\n node.type === 'CallExpression' &&\n node.callee.type === 'Identifier' &&\n node.callee.name in BUNDLED_SUMMARIES\n ) {\n return node.callee.name\n }\n // Resolver forms: () => …; scan the body for m.jimpEngine(…) / jimpEngine(…).\n if (node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression') {\n let found: string | undefined\n const visit = (n: TSESTree.Node): void => {\n if (found) return\n if (n.type === 'CallExpression') {\n if (n.callee.type === 'Identifier' && n.callee.name in BUNDLED_SUMMARIES) {\n found = n.callee.name\n } else if (\n n.callee.type === 'MemberExpression' &&\n n.callee.property.type === 'Identifier' &&\n n.callee.property.name in BUNDLED_SUMMARIES\n ) {\n found = n.callee.property.name\n }\n }\n for (const key of Object.keys(n)) {\n if (key === 'parent') continue\n const value = (n as unknown as Record<string, unknown>)[key]\n const children = Array.isArray(value) ? value : [value]\n for (const child of children) {\n if (!child || typeof child !== 'object') continue\n const childNode = child as TSESTree.Node\n if (typeof childNode.type !== 'string') continue\n visit(childNode)\n }\n }\n }\n visit(node.body)\n return found\n }\n return undefined\n}\n\n/** Derive a capability summary from an element, or undefined when not statically known. */\nconst summarize = (node: TSESTree.Node): EngineSummary | undefined => {\n const factory = bundledFactoryName(node)\n if (factory) return BUNDLED_SUMMARIES[factory]\n if (node.type !== 'ObjectExpression') return undefined\n const idNode = propOf(node, 'id')\n const label =\n idNode?.type === 'Literal' && typeof idNode.value === 'string' ? idNode.value : 'engine'\n const summary: EngineSummary = { label, mutates: [], converts: [] }\n const mutates = propOf(node, 'mutates')\n if (mutates) {\n if (mutates.type !== 'ArrayExpression') return undefined\n for (const el of mutates.elements) {\n if (!el || el.type !== 'ObjectExpression') return undefined\n const over = literalStrings(propOf(el, 'over'))\n const ops = literalStrings(propOf(el, 'ops'))\n const encodes = literalStrings(propOf(el, 'encodes'))\n if (!over || !ops || !encodes) return undefined\n summary.mutates.push({ over, ops, encodes })\n }\n }\n const converts = propOf(node, 'converts')\n if (converts) {\n if (converts.type !== 'ArrayExpression') return undefined\n for (const el of converts.elements) {\n if (!el || el.type !== 'ObjectExpression') return undefined\n const from = literalStrings(propOf(el, 'from'))\n const to = literalStrings(propOf(el, 'to'))\n if (!from || !to) return undefined\n summary.converts.push({ from, to })\n }\n }\n if (summary.mutates.length + summary.converts.length === 0) return undefined\n return summary\n}\n\nconst patternCovers = (broad: string, narrow: string): boolean => {\n if (broad === narrow) return true\n if (broad.endsWith('/*')) return narrow.startsWith(broad.slice(0, -1))\n return false\n}\n\nconst patternsCover = (broad: string[], narrow: string[]): boolean =>\n narrow.every((n) => broad.some((b) => patternCovers(b, n)))\n\nconst subset = (sup: string[], sub: string[]): boolean => sub.every((s) => sup.includes(s))\n\n/** `true` when EVERY capability of `later` is subsumed by some capability of `earlier`. */\nconst shadows = (earlier: EngineSummary, later: EngineSummary): boolean => {\n if (later.mutates.length + later.converts.length === 0) return false\n const mutatesCovered = later.mutates.every((lm) =>\n earlier.mutates.some(\n (em) =>\n patternsCover(em.over, lm.over) && subset(em.ops, lm.ops) && subset(em.encodes, lm.encodes)\n )\n )\n const convertsCovered = later.converts.every((lc) =>\n earlier.converts.some((ec) => patternsCover(ec.from, lc.from) && subset(ec.to, lc.to))\n )\n return mutatesCovered && convertsCovered\n}\n\n/**\n * Flags an engines-array element whose statically-known capabilities are entirely covered by\n * an EARLIER element — dispatch is first-capable-wins, so the later engine is dead code.\n */\nconst noShadowedEngine = createRule({\n name: 'no-shadowed-engine',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'An engine whose declared capabilities are a subset of an earlier engine in the array can never be selected — first-capable-wins dispatch makes it dead code. Reorder (narrow before broad) or remove it.',\n },\n schema: [],\n messages: {\n shadowed:\n 'Engine \"{{later}}\" can never be selected: \"{{earlier}}\" appears earlier in the array and declares a superset of its capabilities (dispatch is first-capable-wins). Put the narrower engine first, or remove it. Opt out with an eslint-disable-next-line adk-media/no-shadowed-engine comment + reason.',\n },\n },\n defaultOptions: [],\n create(context) {\n return {\n CallExpression(node: TSESTree.CallExpression) {\n if (node.callee.type !== 'Identifier' || node.callee.name !== 'createMediaPipeline') {\n return\n }\n const config = node.arguments[0]\n if (!config || config.type !== 'ObjectExpression') return\n const engines = propOf(config, 'engines')\n if (!engines || engines.type !== 'ArrayExpression') return\n const summaries = engines.elements.map((el) => (el ? summarize(el) : undefined))\n for (let i = 1; i < summaries.length; i++) {\n const later = summaries[i]\n if (!later) continue\n for (let j = 0; j < i; j++) {\n const earlier = summaries[j]\n if (!earlier) continue\n if (shadows(earlier, later)) {\n context.report({\n node: engines.elements[i]!,\n messageId: 'shadowed',\n data: { earlier: earlier.label, later: later.label },\n })\n break\n }\n }\n }\n },\n }\n },\n})\n\n// ── augment-contracts-module ─────────────────────────────────────────────────\n\nconst CONTRACTS_SUBPATH = '@nhtio/adk/batteries/media/contracts'\n\n/**\n * Flags a `ConvertOptions` declaration-merging block whose module specifier is not the\n * contracts subpath. Augmenting the barrel (or any other module) silently does nothing —\n * the keys never merge, and the typo costs hours.\n */\nconst augmentContractsModule = createRule({\n name: 'augment-contracts-module',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'ConvertOptions augmentation must target the contracts subpath — declaration merging against any other module specifier silently fails to merge.',\n },\n schema: [],\n messages: {\n wrongModule:\n 'This ConvertOptions augmentation targets \"{{actual}}\", so it will silently never merge. Declaration merging must target the module that declares the interface: declare module \\'{{expected}}\\'.',\n },\n },\n defaultOptions: [],\n create(context) {\n return {\n TSModuleDeclaration(node: TSESTree.TSModuleDeclaration) {\n if (node.global) return\n if (node.id.type !== 'Literal' || typeof node.id.value !== 'string') return\n const specifier = node.id.value\n if (specifier === CONTRACTS_SUBPATH || specifier.endsWith('/media/contracts')) return\n // Only flag specifiers that are plausibly aimed at this battery.\n if (!specifier.includes('media') && !specifier.includes('@nhtio/adk')) return\n const body = node.body\n if (!body || body.type !== 'TSModuleBlock') return\n const declaresConvertOptions = body.body.some(\n (stmt) => stmt.type === 'TSInterfaceDeclaration' && stmt.id.name === 'ConvertOptions'\n )\n if (!declaresConvertOptions) return\n context.report({\n node: node.id,\n messageId: 'wrongModule',\n data: { actual: specifier, expected: CONTRACTS_SUBPATH },\n })\n },\n }\n },\n})\n\n// ── plugin assembly ──────────────────────────────────────────────────────────\n\n/**\n * Map of rule id (without the `adk-media/` plugin prefix) to its rule object. Registered on\n * the plugin as `rules`, so configs reference them as `adk-media/{id}`.\n */\nexport const rules = {\n 'prefer-engine-resolver': preferEngineResolver,\n 'no-shadowed-engine': noShadowedEngine,\n 'augment-contracts-module': augmentContractsModule,\n} satisfies FlatConfig.Plugin['rules']\n\n/**\n * The media battery's ESLint plugin object. Register under the `adk-media` namespace:\n * `plugins: { 'adk-media': plugin }`.\n */\nexport const plugin: FlatConfig.Plugin = {\n meta: { name: '@nhtio/adk/batteries/media/lint', version: __VERSION__ },\n rules,\n}\n\nconst recommendedRules: NonNullable<FlatConfig.Config['rules']> = Object.fromEntries(\n Object.keys(rules).map((id) => [`adk-media/${id}`, 'error'])\n)\n\n/**\n * Named config presets. `recommended` enables every rule at `error` and registers the plugin\n * under the `adk-media` namespace — spread it into a flat config to adopt the full set.\n */\nexport const configs = {\n recommended: {\n name: '@nhtio/adk/batteries/media/lint/recommended',\n plugins: { 'adk-media': plugin },\n rules: recommendedRules,\n } satisfies FlatConfig.Config,\n}\n\n/**\n * Default export bundles the plugin and its config presets, mirroring the shape ESLint flat\n * configs expect from a plugin module.\n */\nexport default { plugin, rules, configs }\n"],"mappings":"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"}
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
27
27
|
import type { FlatConfig } from '@typescript-eslint/utils/ts-eslint';
|
|
28
28
|
/** A statically-derived capability summary for one engines-array element. */
|
|
29
|
-
interface EngineSummary {
|
|
29
|
+
export interface EngineSummary {
|
|
30
30
|
/** Display label for the diagnostic. */
|
|
31
31
|
label: string;
|
|
32
32
|
/** Mutate groups: input patterns, ops, encodes. */
|
package/batteries/media/lint.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lint.mjs","names":[],"sources":["../../../src/batteries/media/lint.ts"],"sourcesContent":["/**\n * @module @nhtio/adk/batteries/media/lint\n *\n * ESLint rules for consumers of the media pipeline battery: machine-checkable enforcement of\n * the engine-composition contracts implementors are most likely to get wrong.\n *\n * @remarks\n * Battery-scoped rules ship with the battery, not with the core `@nhtio/adk/eslint` plugin —\n * they only matter to deployments that compose this battery, and they version with the\n * battery's own API. The rules are report-only (no autofix); carve out a deliberate exception\n * with an inline `// eslint-disable-next-line adk-media/{rule} -- {reason}` comment.\n *\n * `@typescript-eslint/utils` and `eslint` are OPTIONAL peer dependencies of `@nhtio/adk` —\n * installed only by consumers who lint with this plugin. The battery never imports this\n * module at runtime.\n *\n * @example Flat config\n * ```ts\n * import adkMedia from '@nhtio/adk/batteries/media/lint'\n *\n * export default [\n * { plugins: { 'adk-media': adkMedia.plugin }, rules: adkMedia.configs.recommended.rules },\n * ]\n * ```\n */\n\nimport { ESLintUtils } from '@typescript-eslint/utils'\nimport type { TSESTree } from '@typescript-eslint/utils'\nimport type { FlatConfig } from '@typescript-eslint/utils/ts-eslint'\n\nconst createRule = ESLintUtils.RuleCreator(\n (name) => `https://adk.nht.io/batteries/media/lint#${name}`\n)\n\n// ── prefer-engine-resolver ───────────────────────────────────────────────────\n\nconst ENGINE_SUBPATH = /^@nhtio\\/adk\\/batteries\\/media\\/engines\\//\n\n/**\n * Flags a static value import of a bundled engine subpath. The documented canonical supply\n * form is the dynamic-import resolver, which keeps the engine wrapper module out of every\n * bundle that merely might use it. Type-only imports are fine.\n */\nconst preferEngineResolver = createRule({\n name: 'prefer-engine-resolver',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Supply bundled media engines as dynamic-import resolvers, not static imports — a static import puts the engine wrapper in every bundle, even when the engine is conditional.',\n },\n schema: [],\n messages: {\n preferResolver:\n \"Import bundled engines lazily: supply `() => import('{{source}}').then((m) => m.{{hint}}(…))` in the engines array instead of a static import. Static imports put the engine wrapper module in every bundle. Opt out with an eslint-disable-next-line adk-media/prefer-engine-resolver comment + reason.\",\n },\n },\n defaultOptions: [],\n create(context) {\n return {\n ImportDeclaration(node: TSESTree.ImportDeclaration) {\n if (typeof node.source.value !== 'string') return\n if (!ENGINE_SUBPATH.test(node.source.value)) return\n if (node.importKind === 'type') return\n const valueSpecifiers = node.specifiers.filter(\n (s) => !(s.type === 'ImportSpecifier' && s.importKind === 'type')\n )\n if (valueSpecifiers.length === 0) return\n const first = valueSpecifiers[0]\n const hint =\n first.type === 'ImportSpecifier' && first.imported.type === 'Identifier'\n ? first.imported.name\n : 'engineFactory'\n context.report({\n node,\n messageId: 'preferResolver',\n data: { source: node.source.value, hint },\n })\n },\n }\n },\n})\n\n// ── no-shadowed-engine ───────────────────────────────────────────────────────\n\n/** A statically-derived capability summary for one engines-array element. */\ninterface EngineSummary {\n /** Display label for the diagnostic. */\n label: string\n /** Mutate groups: input patterns, ops, encodes. */\n mutates: Array<{ over: string[]; ops: string[]; encodes: string[] }>\n /** Convert groups: input patterns, target tokens. */\n converts: Array<{ from: string[]; to: string[] }>\n}\n\n/**\n * Known declarations of the bundled engine factories, for shadow analysis. An ESLint rule\n * cannot execute the factories it lints, so this table mirrors their declarations by hand —\n * and is pinned against the live factories by a drift test (`lint_drift.node.spec.ts`).\n * Exported for that test only.\n */\nexport const BUNDLED_SUMMARIES: Record<string, EngineSummary> = {\n jimpEngine: {\n label: 'jimpEngine',\n mutates: [\n {\n over: ['image/png', 'image/jpeg', 'image/bmp', 'image/gif', 'image/tiff'],\n ops: ['resize', 'rotate', 'flip', 'strip_metadata'],\n encodes: ['png', 'jpg', 'jpeg', 'bmp', 'gif', 'tiff'],\n },\n ],\n converts: [],\n },\n sharpEngine: {\n label: 'sharpEngine',\n mutates: [\n {\n over: ['image/*'],\n ops: ['resize', 'rotate', 'flip', 'strip_metadata'],\n encodes: ['png', 'jpg', 'jpeg', 'webp', 'tiff', 'avif', 'gif'],\n },\n ],\n converts: [],\n },\n}\n\nconst literalStrings = (node: TSESTree.Node | undefined): string[] | undefined => {\n if (!node || node.type !== 'ArrayExpression') return undefined\n const out: string[] = []\n for (const el of node.elements) {\n if (!el || el.type !== 'Literal' || typeof el.value !== 'string') return undefined\n out.push(el.value)\n }\n return out\n}\n\nconst propOf = (obj: TSESTree.ObjectExpression, name: string): TSESTree.Node | undefined =>\n obj.properties.find(\n (p): p is TSESTree.Property =>\n p.type === 'Property' && !p.computed && p.key.type === 'Identifier' && p.key.name === name\n )?.value\n\n/** Find the bundled factory name referenced by an element (direct call or resolver arrow). */\nconst bundledFactoryName = (node: TSESTree.Node): string | undefined => {\n if (\n node.type === 'CallExpression' &&\n node.callee.type === 'Identifier' &&\n node.callee.name in BUNDLED_SUMMARIES\n ) {\n return node.callee.name\n }\n // Resolver forms: () => …; scan the body for m.jimpEngine(…) / jimpEngine(…).\n if (node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression') {\n let found: string | undefined\n const visit = (n: TSESTree.Node): void => {\n if (found) return\n if (n.type === 'CallExpression') {\n if (n.callee.type === 'Identifier' && n.callee.name in BUNDLED_SUMMARIES) {\n found = n.callee.name\n } else if (\n n.callee.type === 'MemberExpression' &&\n n.callee.property.type === 'Identifier' &&\n n.callee.property.name in BUNDLED_SUMMARIES\n ) {\n found = n.callee.property.name\n }\n }\n for (const key of Object.keys(n)) {\n if (key === 'parent') continue\n const value = (n as unknown as Record<string, unknown>)[key]\n const children = Array.isArray(value) ? value : [value]\n for (const child of children) {\n if (!child || typeof child !== 'object') continue\n const childNode = child as TSESTree.Node\n if (typeof childNode.type !== 'string') continue\n visit(childNode)\n }\n }\n }\n visit(node.body)\n return found\n }\n return undefined\n}\n\n/** Derive a capability summary from an element, or undefined when not statically known. */\nconst summarize = (node: TSESTree.Node): EngineSummary | undefined => {\n const factory = bundledFactoryName(node)\n if (factory) return BUNDLED_SUMMARIES[factory]\n if (node.type !== 'ObjectExpression') return undefined\n const idNode = propOf(node, 'id')\n const label =\n idNode?.type === 'Literal' && typeof idNode.value === 'string' ? idNode.value : 'engine'\n const summary: EngineSummary = { label, mutates: [], converts: [] }\n const mutates = propOf(node, 'mutates')\n if (mutates) {\n if (mutates.type !== 'ArrayExpression') return undefined\n for (const el of mutates.elements) {\n if (!el || el.type !== 'ObjectExpression') return undefined\n const over = literalStrings(propOf(el, 'over'))\n const ops = literalStrings(propOf(el, 'ops'))\n const encodes = literalStrings(propOf(el, 'encodes'))\n if (!over || !ops || !encodes) return undefined\n summary.mutates.push({ over, ops, encodes })\n }\n }\n const converts = propOf(node, 'converts')\n if (converts) {\n if (converts.type !== 'ArrayExpression') return undefined\n for (const el of converts.elements) {\n if (!el || el.type !== 'ObjectExpression') return undefined\n const from = literalStrings(propOf(el, 'from'))\n const to = literalStrings(propOf(el, 'to'))\n if (!from || !to) return undefined\n summary.converts.push({ from, to })\n }\n }\n if (summary.mutates.length + summary.converts.length === 0) return undefined\n return summary\n}\n\nconst patternCovers = (broad: string, narrow: string): boolean => {\n if (broad === narrow) return true\n if (broad.endsWith('/*')) return narrow.startsWith(broad.slice(0, -1))\n return false\n}\n\nconst patternsCover = (broad: string[], narrow: string[]): boolean =>\n narrow.every((n) => broad.some((b) => patternCovers(b, n)))\n\nconst subset = (sup: string[], sub: string[]): boolean => sub.every((s) => sup.includes(s))\n\n/** `true` when EVERY capability of `later` is subsumed by some capability of `earlier`. */\nconst shadows = (earlier: EngineSummary, later: EngineSummary): boolean => {\n if (later.mutates.length + later.converts.length === 0) return false\n const mutatesCovered = later.mutates.every((lm) =>\n earlier.mutates.some(\n (em) =>\n patternsCover(em.over, lm.over) && subset(em.ops, lm.ops) && subset(em.encodes, lm.encodes)\n )\n )\n const convertsCovered = later.converts.every((lc) =>\n earlier.converts.some((ec) => patternsCover(ec.from, lc.from) && subset(ec.to, lc.to))\n )\n return mutatesCovered && convertsCovered\n}\n\n/**\n * Flags an engines-array element whose statically-known capabilities are entirely covered by\n * an EARLIER element — dispatch is first-capable-wins, so the later engine is dead code.\n */\nconst noShadowedEngine = createRule({\n name: 'no-shadowed-engine',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'An engine whose declared capabilities are a subset of an earlier engine in the array can never be selected — first-capable-wins dispatch makes it dead code. Reorder (narrow before broad) or remove it.',\n },\n schema: [],\n messages: {\n shadowed:\n 'Engine \"{{later}}\" can never be selected: \"{{earlier}}\" appears earlier in the array and declares a superset of its capabilities (dispatch is first-capable-wins). Put the narrower engine first, or remove it. Opt out with an eslint-disable-next-line adk-media/no-shadowed-engine comment + reason.',\n },\n },\n defaultOptions: [],\n create(context) {\n return {\n CallExpression(node: TSESTree.CallExpression) {\n if (node.callee.type !== 'Identifier' || node.callee.name !== 'createMediaPipeline') {\n return\n }\n const config = node.arguments[0]\n if (!config || config.type !== 'ObjectExpression') return\n const engines = propOf(config, 'engines')\n if (!engines || engines.type !== 'ArrayExpression') return\n const summaries = engines.elements.map((el) => (el ? summarize(el) : undefined))\n for (let i = 1; i < summaries.length; i++) {\n const later = summaries[i]\n if (!later) continue\n for (let j = 0; j < i; j++) {\n const earlier = summaries[j]\n if (!earlier) continue\n if (shadows(earlier, later)) {\n context.report({\n node: engines.elements[i]!,\n messageId: 'shadowed',\n data: { earlier: earlier.label, later: later.label },\n })\n break\n }\n }\n }\n },\n }\n },\n})\n\n// ── augment-contracts-module ─────────────────────────────────────────────────\n\nconst CONTRACTS_SUBPATH = '@nhtio/adk/batteries/media/contracts'\n\n/**\n * Flags a `ConvertOptions` declaration-merging block whose module specifier is not the\n * contracts subpath. Augmenting the barrel (or any other module) silently does nothing —\n * the keys never merge, and the typo costs hours.\n */\nconst augmentContractsModule = createRule({\n name: 'augment-contracts-module',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'ConvertOptions augmentation must target the contracts subpath — declaration merging against any other module specifier silently fails to merge.',\n },\n schema: [],\n messages: {\n wrongModule:\n 'This ConvertOptions augmentation targets \"{{actual}}\", so it will silently never merge. Declaration merging must target the module that declares the interface: declare module \\'{{expected}}\\'.',\n },\n },\n defaultOptions: [],\n create(context) {\n return {\n TSModuleDeclaration(node: TSESTree.TSModuleDeclaration) {\n if (node.global) return\n if (node.id.type !== 'Literal' || typeof node.id.value !== 'string') return\n const specifier = node.id.value\n if (specifier === CONTRACTS_SUBPATH || specifier.endsWith('/media/contracts')) return\n // Only flag specifiers that are plausibly aimed at this battery.\n if (!specifier.includes('media') && !specifier.includes('@nhtio/adk')) return\n const body = node.body\n if (!body || body.type !== 'TSModuleBlock') return\n const declaresConvertOptions = body.body.some(\n (stmt) => stmt.type === 'TSInterfaceDeclaration' && stmt.id.name === 'ConvertOptions'\n )\n if (!declaresConvertOptions) return\n context.report({\n node: node.id,\n messageId: 'wrongModule',\n data: { actual: specifier, expected: CONTRACTS_SUBPATH },\n })\n },\n }\n },\n})\n\n// ── plugin assembly ──────────────────────────────────────────────────────────\n\n/**\n * Map of rule id (without the `adk-media/` plugin prefix) to its rule object. Registered on\n * the plugin as `rules`, so configs reference them as `adk-media/{id}`.\n */\nexport const rules = {\n 'prefer-engine-resolver': preferEngineResolver,\n 'no-shadowed-engine': noShadowedEngine,\n 'augment-contracts-module': augmentContractsModule,\n} satisfies FlatConfig.Plugin['rules']\n\n/**\n * The media battery's ESLint plugin object. Register under the `adk-media` namespace:\n * `plugins: { 'adk-media': plugin }`.\n */\nexport const plugin: FlatConfig.Plugin = {\n meta: { name: '@nhtio/adk/batteries/media/lint', version: __VERSION__ },\n rules,\n}\n\nconst recommendedRules: NonNullable<FlatConfig.Config['rules']> = Object.fromEntries(\n Object.keys(rules).map((id) => [`adk-media/${id}`, 'error'])\n)\n\n/**\n * Named config presets. `recommended` enables every rule at `error` and registers the plugin\n * under the `adk-media` namespace — spread it into a flat config to adopt the full set.\n */\nexport const configs = {\n recommended: {\n name: '@nhtio/adk/batteries/media/lint/recommended',\n plugins: { 'adk-media': plugin },\n rules: recommendedRules,\n } satisfies FlatConfig.Config,\n}\n\n/**\n * Default export bundles the plugin and its config presets, mirroring the shape ESLint flat\n * configs expect from a plugin module.\n */\nexport default { plugin, rules, configs }\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lint.mjs","names":[],"sources":["../../../src/batteries/media/lint.ts"],"sourcesContent":["/**\n * @module @nhtio/adk/batteries/media/lint\n *\n * ESLint rules for consumers of the media pipeline battery: machine-checkable enforcement of\n * the engine-composition contracts implementors are most likely to get wrong.\n *\n * @remarks\n * Battery-scoped rules ship with the battery, not with the core `@nhtio/adk/eslint` plugin —\n * they only matter to deployments that compose this battery, and they version with the\n * battery's own API. The rules are report-only (no autofix); carve out a deliberate exception\n * with an inline `// eslint-disable-next-line adk-media/{rule} -- {reason}` comment.\n *\n * `@typescript-eslint/utils` and `eslint` are OPTIONAL peer dependencies of `@nhtio/adk` —\n * installed only by consumers who lint with this plugin. The battery never imports this\n * module at runtime.\n *\n * @example Flat config\n * ```ts\n * import adkMedia from '@nhtio/adk/batteries/media/lint'\n *\n * export default [\n * { plugins: { 'adk-media': adkMedia.plugin }, rules: adkMedia.configs.recommended.rules },\n * ]\n * ```\n */\n\nimport { ESLintUtils } from '@typescript-eslint/utils'\nimport type { TSESTree } from '@typescript-eslint/utils'\nimport type { FlatConfig } from '@typescript-eslint/utils/ts-eslint'\n\nconst createRule = ESLintUtils.RuleCreator(\n (name) => `https://adk.nht.io/batteries/media/lint#${name}`\n)\n\n// ── prefer-engine-resolver ───────────────────────────────────────────────────\n\nconst ENGINE_SUBPATH = /^@nhtio\\/adk\\/batteries\\/media\\/engines\\//\n\n/**\n * Flags a static value import of a bundled engine subpath. The documented canonical supply\n * form is the dynamic-import resolver, which keeps the engine wrapper module out of every\n * bundle that merely might use it. Type-only imports are fine.\n */\nconst preferEngineResolver = createRule({\n name: 'prefer-engine-resolver',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Supply bundled media engines as dynamic-import resolvers, not static imports — a static import puts the engine wrapper in every bundle, even when the engine is conditional.',\n },\n schema: [],\n messages: {\n preferResolver:\n \"Import bundled engines lazily: supply `() => import('{{source}}').then((m) => m.{{hint}}(…))` in the engines array instead of a static import. Static imports put the engine wrapper module in every bundle. Opt out with an eslint-disable-next-line adk-media/prefer-engine-resolver comment + reason.\",\n },\n },\n defaultOptions: [],\n create(context) {\n return {\n ImportDeclaration(node: TSESTree.ImportDeclaration) {\n if (typeof node.source.value !== 'string') return\n if (!ENGINE_SUBPATH.test(node.source.value)) return\n if (node.importKind === 'type') return\n const valueSpecifiers = node.specifiers.filter(\n (s) => !(s.type === 'ImportSpecifier' && s.importKind === 'type')\n )\n if (valueSpecifiers.length === 0) return\n const first = valueSpecifiers[0]\n const hint =\n first.type === 'ImportSpecifier' && first.imported.type === 'Identifier'\n ? first.imported.name\n : 'engineFactory'\n context.report({\n node,\n messageId: 'preferResolver',\n data: { source: node.source.value, hint },\n })\n },\n }\n },\n})\n\n// ── no-shadowed-engine ───────────────────────────────────────────────────────\n\n/** A statically-derived capability summary for one engines-array element. */\nexport interface EngineSummary {\n /** Display label for the diagnostic. */\n label: string\n /** Mutate groups: input patterns, ops, encodes. */\n mutates: Array<{ over: string[]; ops: string[]; encodes: string[] }>\n /** Convert groups: input patterns, target tokens. */\n converts: Array<{ from: string[]; to: string[] }>\n}\n\n/**\n * Known declarations of the bundled engine factories, for shadow analysis. An ESLint rule\n * cannot execute the factories it lints, so this table mirrors their declarations by hand —\n * and is pinned against the live factories by a drift test (`lint_drift.node.spec.ts`).\n * Exported for that test only.\n */\nexport const BUNDLED_SUMMARIES: Record<string, EngineSummary> = {\n jimpEngine: {\n label: 'jimpEngine',\n mutates: [\n {\n over: ['image/png', 'image/jpeg', 'image/bmp', 'image/gif', 'image/tiff'],\n ops: ['resize', 'rotate', 'flip', 'strip_metadata'],\n encodes: ['png', 'jpg', 'jpeg', 'bmp', 'gif', 'tiff'],\n },\n ],\n converts: [],\n },\n sharpEngine: {\n label: 'sharpEngine',\n mutates: [\n {\n over: ['image/*'],\n ops: ['resize', 'rotate', 'flip', 'strip_metadata'],\n encodes: ['png', 'jpg', 'jpeg', 'webp', 'tiff', 'avif', 'gif'],\n },\n ],\n converts: [],\n },\n}\n\nconst literalStrings = (node: TSESTree.Node | undefined): string[] | undefined => {\n if (!node || node.type !== 'ArrayExpression') return undefined\n const out: string[] = []\n for (const el of node.elements) {\n if (!el || el.type !== 'Literal' || typeof el.value !== 'string') return undefined\n out.push(el.value)\n }\n return out\n}\n\nconst propOf = (obj: TSESTree.ObjectExpression, name: string): TSESTree.Node | undefined =>\n obj.properties.find(\n (p): p is TSESTree.Property =>\n p.type === 'Property' && !p.computed && p.key.type === 'Identifier' && p.key.name === name\n )?.value\n\n/** Find the bundled factory name referenced by an element (direct call or resolver arrow). */\nconst bundledFactoryName = (node: TSESTree.Node): string | undefined => {\n if (\n node.type === 'CallExpression' &&\n node.callee.type === 'Identifier' &&\n node.callee.name in BUNDLED_SUMMARIES\n ) {\n return node.callee.name\n }\n // Resolver forms: () => …; scan the body for m.jimpEngine(…) / jimpEngine(…).\n if (node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression') {\n let found: string | undefined\n const visit = (n: TSESTree.Node): void => {\n if (found) return\n if (n.type === 'CallExpression') {\n if (n.callee.type === 'Identifier' && n.callee.name in BUNDLED_SUMMARIES) {\n found = n.callee.name\n } else if (\n n.callee.type === 'MemberExpression' &&\n n.callee.property.type === 'Identifier' &&\n n.callee.property.name in BUNDLED_SUMMARIES\n ) {\n found = n.callee.property.name\n }\n }\n for (const key of Object.keys(n)) {\n if (key === 'parent') continue\n const value = (n as unknown as Record<string, unknown>)[key]\n const children = Array.isArray(value) ? value : [value]\n for (const child of children) {\n if (!child || typeof child !== 'object') continue\n const childNode = child as TSESTree.Node\n if (typeof childNode.type !== 'string') continue\n visit(childNode)\n }\n }\n }\n visit(node.body)\n return found\n }\n return undefined\n}\n\n/** Derive a capability summary from an element, or undefined when not statically known. */\nconst summarize = (node: TSESTree.Node): EngineSummary | undefined => {\n const factory = bundledFactoryName(node)\n if (factory) return BUNDLED_SUMMARIES[factory]\n if (node.type !== 'ObjectExpression') return undefined\n const idNode = propOf(node, 'id')\n const label =\n idNode?.type === 'Literal' && typeof idNode.value === 'string' ? idNode.value : 'engine'\n const summary: EngineSummary = { label, mutates: [], converts: [] }\n const mutates = propOf(node, 'mutates')\n if (mutates) {\n if (mutates.type !== 'ArrayExpression') return undefined\n for (const el of mutates.elements) {\n if (!el || el.type !== 'ObjectExpression') return undefined\n const over = literalStrings(propOf(el, 'over'))\n const ops = literalStrings(propOf(el, 'ops'))\n const encodes = literalStrings(propOf(el, 'encodes'))\n if (!over || !ops || !encodes) return undefined\n summary.mutates.push({ over, ops, encodes })\n }\n }\n const converts = propOf(node, 'converts')\n if (converts) {\n if (converts.type !== 'ArrayExpression') return undefined\n for (const el of converts.elements) {\n if (!el || el.type !== 'ObjectExpression') return undefined\n const from = literalStrings(propOf(el, 'from'))\n const to = literalStrings(propOf(el, 'to'))\n if (!from || !to) return undefined\n summary.converts.push({ from, to })\n }\n }\n if (summary.mutates.length + summary.converts.length === 0) return undefined\n return summary\n}\n\nconst patternCovers = (broad: string, narrow: string): boolean => {\n if (broad === narrow) return true\n if (broad.endsWith('/*')) return narrow.startsWith(broad.slice(0, -1))\n return false\n}\n\nconst patternsCover = (broad: string[], narrow: string[]): boolean =>\n narrow.every((n) => broad.some((b) => patternCovers(b, n)))\n\nconst subset = (sup: string[], sub: string[]): boolean => sub.every((s) => sup.includes(s))\n\n/** `true` when EVERY capability of `later` is subsumed by some capability of `earlier`. */\nconst shadows = (earlier: EngineSummary, later: EngineSummary): boolean => {\n if (later.mutates.length + later.converts.length === 0) return false\n const mutatesCovered = later.mutates.every((lm) =>\n earlier.mutates.some(\n (em) =>\n patternsCover(em.over, lm.over) && subset(em.ops, lm.ops) && subset(em.encodes, lm.encodes)\n )\n )\n const convertsCovered = later.converts.every((lc) =>\n earlier.converts.some((ec) => patternsCover(ec.from, lc.from) && subset(ec.to, lc.to))\n )\n return mutatesCovered && convertsCovered\n}\n\n/**\n * Flags an engines-array element whose statically-known capabilities are entirely covered by\n * an EARLIER element — dispatch is first-capable-wins, so the later engine is dead code.\n */\nconst noShadowedEngine = createRule({\n name: 'no-shadowed-engine',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'An engine whose declared capabilities are a subset of an earlier engine in the array can never be selected — first-capable-wins dispatch makes it dead code. Reorder (narrow before broad) or remove it.',\n },\n schema: [],\n messages: {\n shadowed:\n 'Engine \"{{later}}\" can never be selected: \"{{earlier}}\" appears earlier in the array and declares a superset of its capabilities (dispatch is first-capable-wins). Put the narrower engine first, or remove it. Opt out with an eslint-disable-next-line adk-media/no-shadowed-engine comment + reason.',\n },\n },\n defaultOptions: [],\n create(context) {\n return {\n CallExpression(node: TSESTree.CallExpression) {\n if (node.callee.type !== 'Identifier' || node.callee.name !== 'createMediaPipeline') {\n return\n }\n const config = node.arguments[0]\n if (!config || config.type !== 'ObjectExpression') return\n const engines = propOf(config, 'engines')\n if (!engines || engines.type !== 'ArrayExpression') return\n const summaries = engines.elements.map((el) => (el ? summarize(el) : undefined))\n for (let i = 1; i < summaries.length; i++) {\n const later = summaries[i]\n if (!later) continue\n for (let j = 0; j < i; j++) {\n const earlier = summaries[j]\n if (!earlier) continue\n if (shadows(earlier, later)) {\n context.report({\n node: engines.elements[i]!,\n messageId: 'shadowed',\n data: { earlier: earlier.label, later: later.label },\n })\n break\n }\n }\n }\n },\n }\n },\n})\n\n// ── augment-contracts-module ─────────────────────────────────────────────────\n\nconst CONTRACTS_SUBPATH = '@nhtio/adk/batteries/media/contracts'\n\n/**\n * Flags a `ConvertOptions` declaration-merging block whose module specifier is not the\n * contracts subpath. Augmenting the barrel (or any other module) silently does nothing —\n * the keys never merge, and the typo costs hours.\n */\nconst augmentContractsModule = createRule({\n name: 'augment-contracts-module',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'ConvertOptions augmentation must target the contracts subpath — declaration merging against any other module specifier silently fails to merge.',\n },\n schema: [],\n messages: {\n wrongModule:\n 'This ConvertOptions augmentation targets \"{{actual}}\", so it will silently never merge. Declaration merging must target the module that declares the interface: declare module \\'{{expected}}\\'.',\n },\n },\n defaultOptions: [],\n create(context) {\n return {\n TSModuleDeclaration(node: TSESTree.TSModuleDeclaration) {\n if (node.global) return\n if (node.id.type !== 'Literal' || typeof node.id.value !== 'string') return\n const specifier = node.id.value\n if (specifier === CONTRACTS_SUBPATH || specifier.endsWith('/media/contracts')) return\n // Only flag specifiers that are plausibly aimed at this battery.\n if (!specifier.includes('media') && !specifier.includes('@nhtio/adk')) return\n const body = node.body\n if (!body || body.type !== 'TSModuleBlock') return\n const declaresConvertOptions = body.body.some(\n (stmt) => stmt.type === 'TSInterfaceDeclaration' && stmt.id.name === 'ConvertOptions'\n )\n if (!declaresConvertOptions) return\n context.report({\n node: node.id,\n messageId: 'wrongModule',\n data: { actual: specifier, expected: CONTRACTS_SUBPATH },\n })\n },\n }\n },\n})\n\n// ── plugin assembly ──────────────────────────────────────────────────────────\n\n/**\n * Map of rule id (without the `adk-media/` plugin prefix) to its rule object. Registered on\n * the plugin as `rules`, so configs reference them as `adk-media/{id}`.\n */\nexport const rules = {\n 'prefer-engine-resolver': preferEngineResolver,\n 'no-shadowed-engine': noShadowedEngine,\n 'augment-contracts-module': augmentContractsModule,\n} satisfies FlatConfig.Plugin['rules']\n\n/**\n * The media battery's ESLint plugin object. Register under the `adk-media` namespace:\n * `plugins: { 'adk-media': plugin }`.\n */\nexport const plugin: FlatConfig.Plugin = {\n meta: { name: '@nhtio/adk/batteries/media/lint', version: __VERSION__ },\n rules,\n}\n\nconst recommendedRules: NonNullable<FlatConfig.Config['rules']> = Object.fromEntries(\n Object.keys(rules).map((id) => [`adk-media/${id}`, 'error'])\n)\n\n/**\n * Named config presets. `recommended` enables every rule at `error` and registers the plugin\n * under the `adk-media` namespace — spread it into a flat config to adopt the full set.\n */\nexport const configs = {\n recommended: {\n name: '@nhtio/adk/batteries/media/lint/recommended',\n plugins: { 'adk-media': plugin },\n rules: recommendedRules,\n } satisfies FlatConfig.Config,\n}\n\n/**\n * Default export bundles the plugin and its config presets, mirroring the shape ESLint flat\n * configs expect from a plugin module.\n */\nexport default { plugin, rules, configs }\n"],"mappings":"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"}
|
|
@@ -49,7 +49,7 @@ export interface RawSegment {
|
|
|
49
49
|
* @remarks
|
|
50
50
|
* Purely syntactic — verbs are fold-matched against the full verb table for canonicalization
|
|
51
51
|
* but unknown verbs are NOT an error here (the validator reports them with did-you-mean and the
|
|
52
|
-
* deployment's narrowed verb list).
|
|
52
|
+
* deployment's narrowed verb list). Pair with `validateSegments` for the validated path.
|
|
53
53
|
*
|
|
54
54
|
* @param input - The pipe expression.
|
|
55
55
|
* @returns The raw segments.
|
package/batteries/media.cjs
CHANGED
|
@@ -279,7 +279,7 @@ var asScalarList = (raw, verbText, argName) => {
|
|
|
279
279
|
* @remarks
|
|
280
280
|
* Purely syntactic — verbs are fold-matched against the full verb table for canonicalization
|
|
281
281
|
* but unknown verbs are NOT an error here (the validator reports them with did-you-mean and the
|
|
282
|
-
* deployment's narrowed verb list).
|
|
282
|
+
* deployment's narrowed verb list). Pair with `validateSegments` for the validated path.
|
|
283
283
|
*
|
|
284
284
|
* @param input - The pipe expression.
|
|
285
285
|
* @returns The raw segments.
|