001/*
002 * Copyright 2022-2026 Revetware LLC.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package com.soklet;
017
018import com.soklet.internal.mcp.skills.McpSkillRuntimeBridge;
019import org.jspecify.annotations.NonNull;
020import org.jspecify.annotations.Nullable;
021
022import javax.annotation.concurrent.ThreadSafe;
023import java.util.Map;
024import java.util.Optional;
025import java.util.Set;
026
027/**
028 * Immutable snapshot of application-supplied Skills files and document metadata.
029 *
030 * <p>Logical file paths are bundle-relative names, not filesystem locations.
031 * Construction performs no file access, remote fetching, execution, or endpoint
032 * publication. Applications own loading the bytes before supplying them here.
033 * Metadata and path getters return cached immutable views; only file-byte
034 * inspection creates defensive copies.
035 *
036 * @author <a href="https://www.revetkn.com">Mark Allen</a>
037 */
038@ThreadSafe
039public final class McpSkillBundle {
040        private final McpSkillRuntimeBridge.@NonNull Bundle runtimeBundle;
041
042        private McpSkillBundle(McpSkillRuntimeBridge.@NonNull Bundle runtimeBundle) {
043                this.runtimeBundle = runtimeBundle;
044        }
045
046        /**
047         * Validates and snapshots a bundle, including its required root {@code SKILL.md}.
048         *
049         * <p>Keys must be unique NFC logical paths with no empty, dot, or dot-dot
050         * segments, backslashes, percent signs, or control characters. A file cannot
051         * also be a directory prefix of another file (for example, {@code ref} and
052         * {@code ref/notes.md}). Names are case-sensitive and are not case-folded;
053         * applications loading or materializing files on a case-insensitive filesystem
054         * must account for names that differ only by case. Each path is
055         * limited to 8,192 UTF-8 bytes. Bundles contain at most 512 files and 16 MiB of
056         * raw content; individual resource representations remain subject to the
057         * stricter MCP output limits. Supplied arrays are defensively copied. Do not
058         * mutate the map or its arrays while construction is in progress.
059         *
060         * <p>The complete root document, including its body, must be valid UTF-8 and
061         * fit within 4 MiB. Frontmatter parsing and resolution share fixed bounds:
062         * nesting depth 128; 1,000,000 syntax and resolved nodes; 1,048,576 UTF-16 code
063         * units per scalar; 4,194,304 aggregate charged scalar characters; and
064         * 50,000,000 metered work units. The production JSON profile independently
065         * constrains resolved metadata and resource delivery. These construction
066         * bounds are not configurable through this factory.
067         *
068         * <p>Metadata validation errors identify the fixed field and violated rule
069         * without including authored values or custom metadata keys. The exception
070         * remains an {@link IllegalArgumentException}; diagnostics are not a structured
071         * validation API. Metadata validation does not repair or rewrite the document.
072         *
073         * @param fileContentsByLogicalPath logical file paths and complete file contents
074         * @return immutable validated bundle
075         * @throws NullPointerException if the map, a path, or file bytes are null
076         * @throws IllegalArgumentException if paths, metadata, content, or limits are invalid
077         */
078        @NonNull
079        public static McpSkillBundle fromFiles(
080                        @NonNull Map<@NonNull String, byte @NonNull []> fileContentsByLogicalPath) {
081                return new McpSkillBundle(McpSkillRuntimeBridge.fromFiles(fileContentsByLogicalPath));
082        }
083
084        /** @return validated skill name from the root document */
085        @NonNull
086        public String getName() { return this.runtimeBundle.name(); }
087
088        /** @return skill description from the root document */
089        @NonNull
090        public String getDescription() { return this.runtimeBundle.description(); }
091
092        /**
093         * Returns cached root frontmatter, including supported unknown fields.
094         *
095         * @return immutable complete document metadata, not protocol {@code _meta}
096         */
097        @NonNull
098        public McpJsonObject getDocumentMetadata() { return this.runtimeBundle.documentMetadata(); }
099
100        /**
101         * Returns cached canonical logical paths without filesystem resolution.
102         *
103         * @return immutable ordered set, root {@code SKILL.md} first and remaining
104         * paths in unsigned UTF-8 byte order
105         */
106        @NonNull
107        public Set<@NonNull String> getFilePaths() { return this.runtimeBundle.filePaths(); }
108
109        /**
110         * Copies the requested file without copying the complete bundle.
111         *
112         * @param logicalFilePath exact, case-sensitive logical file path
113         * @return defensive file-byte copy, or empty when the path is absent
114         * @throws NullPointerException if the path is null
115         */
116        @NonNull
117        public Optional<byte @NonNull []> findFileBytes(@NonNull String logicalFilePath) {
118                return this.runtimeBundle.findFileBytes(logicalFilePath);
119        }
120
121        McpSkillRuntimeBridge.@NonNull Bundle runtimeBundle() { return this.runtimeBundle; }
122
123        /** @return whether logical paths and exact snapshot bytes match */
124        @Override
125        public boolean equals(@Nullable Object other) {
126                return this == other || other instanceof McpSkillBundle bundle
127                                && this.runtimeBundle.equals(bundle.runtimeBundle);
128        }
129
130        /** @return structural bundle hash code without copying inspection bytes */
131        @Override
132        public int hashCode() { return this.runtimeBundle.hashCode(); }
133
134        /** @return diagnostic rendering without authored metadata, paths, or bytes */
135        @Override
136        @NonNull
137        public String toString() { return "McpSkillBundle[redacted]"; }
138}