codex-plugin-doctor 1.58.0 → 1.60.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.
@@ -0,0 +1,38 @@
1
+ export const submissionRuleset = Object.freeze({
2
+ version: "openai-directory-2026-08-15",
3
+ reviewedAt: "2026-08-15",
4
+ sources: Object.freeze([
5
+ "https://developers.openai.com/plugins/build/plugins",
6
+ "https://developers.openai.com/plugins/deploy/app-review",
7
+ "https://developers.openai.com/plugins/deploy/submission-errors"
8
+ ]),
9
+ limits: Object.freeze({
10
+ packageName: 64,
11
+ version: 64,
12
+ displayName: 30,
13
+ shortDescription: 30,
14
+ longDescription: 4000,
15
+ developerName: 80,
16
+ capabilities: 20,
17
+ capability: 120,
18
+ starterPrompts: 3,
19
+ starterPrompt: 128,
20
+ url: 1024
21
+ }),
22
+ categories: Object.freeze([
23
+ "Productivity", "Creativity", "Developer Tools", "Business & Operations",
24
+ "Data & Analytics", "Communication", "Education & Research", "Security",
25
+ "Finance", "Healthcare", "Travel", "Entertainment", "Other"
26
+ ])
27
+ });
28
+ export const submissionManualChecks = Object.freeze([
29
+ Object.freeze({ id: "developer-business-identity", label: "Developer and business identity", mcpOnly: false }),
30
+ Object.freeze({ id: "attestations", label: "Required attestations", mcpOnly: false }),
31
+ Object.freeze({ id: "skill-safety-scan", label: "Skill safety scan", mcpOnly: false }),
32
+ Object.freeze({ id: "demo-video", label: "Demo video", mcpOnly: true }),
33
+ Object.freeze({ id: "tool-tests", label: "Exactly 5 positive and 3 negative tool tests", mcpOnly: true }),
34
+ Object.freeze({ id: "release-notes", label: "Release notes", mcpOnly: true }),
35
+ Object.freeze({ id: "production-domain-verification", label: "Production domain verification and current tool scan", mcpOnly: true }),
36
+ Object.freeze({ id: "tool-annotations", label: "Tool annotations and justifications", mcpOnly: true }),
37
+ Object.freeze({ id: "oauth-reviewer-credentials", label: "OAuth reviewer credentials", mcpOnly: true })
38
+ ]);
@@ -0,0 +1,11 @@
1
+ import type { DiscoveredPackage, PluginManifest } from "../domain/types.js";
2
+ import { type SubmissionPackageReader } from "./submission-package-reader.js";
3
+ import type { SubmissionFinding, SubmissionPreflightReport } from "./submission-preflight.js";
4
+ type TargetType = SubmissionPreflightReport["targetType"];
5
+ export interface SubmissionSkillMetadataResult {
6
+ findings: SubmissionFinding[];
7
+ skillCount: number;
8
+ }
9
+ export declare function validateSubmissionSkillMetadataFromReader(manifest: PluginManifest, targetType: SubmissionPreflightReport["targetType"], reader: SubmissionPackageReader): Promise<SubmissionSkillMetadataResult>;
10
+ export declare function validateSubmissionSkillMetadata(discoveredPackage: DiscoveredPackage, targetType: TargetType): Promise<SubmissionSkillMetadataResult>;
11
+ export {};
@@ -0,0 +1,282 @@
1
+ import path from "node:path";
2
+ import { isAlias, isNode, parseDocument, visit } from "yaml";
3
+ import { createDirectorySubmissionPackageReader } from "./submission-package-reader.js";
4
+ const maxSkillBytes = 1024 * 1024;
5
+ const maxAgentBytes = 256 * 1024;
6
+ const maxAggregateMetadataBytes = 16 * 1024 * 1024;
7
+ const maxDirectoryEntries = 256;
8
+ const maxSkillDirectories = 100;
9
+ const unsupportedText = /[\u0000-\u001F\u007F\u2028\u2029\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF]/u;
10
+ const interfaceKeys = new Set(["display_name", "short_description", "icon_small", "icon_large", "brand_color", "default_prompt"]);
11
+ const policyKeys = new Set(["products", "allow_implicit_invocation"]);
12
+ const dependencyKeys = new Set(["tools"]);
13
+ const toolDescriptorKeys = new Set(["type", "value", "description", "transport", "url"]);
14
+ const agentKeys = new Set(["interface", "policy", "dependencies"]);
15
+ function isRecord(value) {
16
+ return typeof value === "object" && value !== null && !Array.isArray(value);
17
+ }
18
+ function finding(id, message, evidence) {
19
+ return evidence === undefined ? { id, severity: "fail", message } : { id, severity: "fail", message, evidence };
20
+ }
21
+ function supportedText(value, limit = Number.MAX_SAFE_INTEGER) {
22
+ return typeof value === "string" && value.trim().length > 0 && value.length <= limit && !unsupportedText.test(value);
23
+ }
24
+ function parseSafeYaml(source) {
25
+ const document = parseDocument(source, { schema: "core", uniqueKeys: true, strict: true, prettyErrors: false });
26
+ let alias = false;
27
+ let nonCoreTag = false;
28
+ visit(document, (_key, node) => {
29
+ if (isAlias(node))
30
+ alias = true;
31
+ if (isNode(node) && node.tag !== undefined && !node.tag.startsWith("tag:yaml.org,2002:"))
32
+ nonCoreTag = true;
33
+ });
34
+ if (document.errors.length > 0 || alias || nonCoreTag)
35
+ return { error: "yaml" };
36
+ try {
37
+ const value = document.toJS({ maxAliasCount: 0 });
38
+ return isRecord(value) ? { value } : { error: "shape" };
39
+ }
40
+ catch {
41
+ return { error: "yaml" };
42
+ }
43
+ }
44
+ function splitSkillFile(source) {
45
+ const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)([\s\S]*)$/u.exec(source);
46
+ return !match || match[2].trim().length === 0 ? null : { frontmatter: match[1], body: match[2] };
47
+ }
48
+ function skillsRoot(value) {
49
+ if (typeof value !== "string" || !value.startsWith("./") || value.includes("\\") || /[\u0000-\u001F\u007F]/u.test(value))
50
+ return null;
51
+ const normalized = path.posix.normalize(value.slice(2)).replace(/\/$/u, "");
52
+ return normalized === "skills" ? normalized : null;
53
+ }
54
+ function relativePackagePath(base, value) {
55
+ if (path.posix.isAbsolute(value) || /^[A-Za-z]:[\\/]/u.test(value))
56
+ return null;
57
+ const resolved = path.posix.normalize(path.posix.join(base, value));
58
+ return resolved === ".." || resolved.startsWith("../") || resolved === "." ? null : resolved;
59
+ }
60
+ function normalizeResolvedPackagePath(value) {
61
+ if (typeof value !== "string" || value === "" || value.includes("\\") || path.posix.isAbsolute(value)
62
+ || /^[A-Za-z]:/u.test(value) || /[\u0000-\u001F\u007F]/u.test(value))
63
+ return null;
64
+ const normalized = path.posix.normalize(value);
65
+ if (normalized === "" || normalized === "." || normalized === ".." || normalized.startsWith("../"))
66
+ return null;
67
+ return normalized;
68
+ }
69
+ function isWithinPackagePath(root, candidate) {
70
+ return candidate === root || candidate.startsWith(`${root}/`);
71
+ }
72
+ function budgetFinding(bytes) {
73
+ return finding("plugin.submission.skill.budget_exceeded", "Skill metadata exceeds the aggregate submission preflight size limit.", {
74
+ count: bytes, limit: maxAggregateMetadataBytes
75
+ });
76
+ }
77
+ async function safeStat(reader, packagePath) {
78
+ return reader.stat(packagePath).catch(() => null);
79
+ }
80
+ async function readSafeUtf8(reader, packagePath, maximum, budget) {
81
+ const details = await safeStat(reader, packagePath);
82
+ if (details === null || details.safeResolution !== "safe" || details.resolvedKind !== "file" || details.size > maximum)
83
+ return { kind: "invalid" };
84
+ const nextBytes = budget.bytes + details.size;
85
+ if (nextBytes > maxAggregateMetadataBytes)
86
+ return { kind: "budget", nextBytes };
87
+ const bytes = await reader.read(packagePath, maximum).catch(() => null);
88
+ if (bytes === null || bytes.byteLength !== details.size)
89
+ return { kind: "invalid" };
90
+ try {
91
+ budget.bytes = nextBytes;
92
+ return { kind: "source", source: new TextDecoder("utf-8", { fatal: true }).decode(bytes) };
93
+ }
94
+ catch {
95
+ return { kind: "invalid" };
96
+ }
97
+ }
98
+ function rejectUnknownKeys(value, allowed) {
99
+ return Object.keys(value).some((key) => !allowed.has(key));
100
+ }
101
+ function isToolDescriptor(value) {
102
+ return isRecord(value) && !rejectUnknownKeys(value, toolDescriptorKeys)
103
+ && (value.type === "mcp" || value.type === "cli")
104
+ && (value.type !== "cli" || (value.transport === undefined && value.url === undefined))
105
+ && supportedText(value.value)
106
+ && (value.description === undefined || supportedText(value.description))
107
+ && (value.transport === undefined || supportedText(value.transport))
108
+ && (value.url === undefined || supportedText(value.url));
109
+ }
110
+ async function validateIconPath(reader, skillRoot, skillPath, field, value) {
111
+ if (typeof value !== "string" || value.trim() !== value || !supportedText(value)) {
112
+ return finding("plugin.submission.skill.agent.invalid_path", "Optional agent icon path is invalid.", { path: skillPath, field });
113
+ }
114
+ const iconPath = relativePackagePath(skillRoot, value);
115
+ const details = iconPath === null ? null : await safeStat(reader, iconPath);
116
+ if (details === null || details.safeResolution !== "safe" || details.resolvedKind !== "file") {
117
+ return finding("plugin.submission.skill.agent.invalid_path", "Optional agent icon path is invalid.", { path: skillPath, field });
118
+ }
119
+ return null;
120
+ }
121
+ async function validateAgentFile(reader, skillRoot, resolvedSkillRoot, budget) {
122
+ const skillPath = skillRoot;
123
+ const agentPath = `${skillRoot}/agents/openai.yaml`;
124
+ const agentDetails = await safeStat(reader, agentPath);
125
+ if (agentDetails === null)
126
+ return [];
127
+ if (agentDetails.kind === "symlink") {
128
+ return [finding("plugin.submission.skill.agent.invalid_path", "Optional agent metadata must not be a symbolic link.", { path: skillPath })];
129
+ }
130
+ if (agentDetails.safeResolution === "outside") {
131
+ return [finding("plugin.submission.skill.agent.invalid_path", "Optional agent metadata resolves outside its skill.", { path: skillPath })];
132
+ }
133
+ if (agentDetails.safeResolution !== "safe") {
134
+ return [finding("plugin.submission.skill.agent.invalid_file", "Optional agent metadata must be a readable regular file.", { path: agentPath })];
135
+ }
136
+ if (agentDetails.resolvedKind !== "file") {
137
+ return [finding("plugin.submission.skill.agent.invalid_file", "Optional agent metadata must be a regular file.", { path: agentPath })];
138
+ }
139
+ const normalizedSkillRoot = normalizeResolvedPackagePath(resolvedSkillRoot);
140
+ const resolvedAgentPath = normalizeResolvedPackagePath(agentDetails.resolvedPackagePath ?? agentPath);
141
+ if (normalizedSkillRoot === null || resolvedAgentPath === null || !isWithinPackagePath(normalizedSkillRoot, resolvedAgentPath)) {
142
+ return [finding("plugin.submission.skill.agent.invalid_path", "Optional agent metadata resolves outside its skill.", { path: skillPath })];
143
+ }
144
+ const source = await readSafeUtf8(reader, agentPath, maxAgentBytes, budget);
145
+ if (source.kind === "budget")
146
+ return [budgetFinding(source.nextBytes)];
147
+ if (source.kind === "invalid") {
148
+ return [finding("plugin.submission.skill.agent.invalid_file", "Optional agent metadata must be a bounded UTF-8 regular file.", { path: agentPath, limit: maxAgentBytes })];
149
+ }
150
+ const parsed = parseSafeYaml(source.source);
151
+ if ("error" in parsed) {
152
+ return [finding(parsed.error === "yaml" ? "plugin.submission.skill.agent.invalid_yaml" : "plugin.submission.skill.agent.invalid_shape", "Optional agent metadata must be a safe YAML mapping.", { path: agentPath })];
153
+ }
154
+ const metadata = parsed.value;
155
+ if (rejectUnknownKeys(metadata, agentKeys) || !isRecord(metadata.interface) || rejectUnknownKeys(metadata.interface, interfaceKeys)
156
+ || !supportedText(metadata.interface.display_name) || !supportedText(metadata.interface.short_description)) {
157
+ return [finding("plugin.submission.skill.agent.invalid_shape", "Optional agent metadata has an unsupported shape.", { path: agentPath })];
158
+ }
159
+ if (metadata.interface.brand_color !== undefined && (typeof metadata.interface.brand_color !== "string" || !/^#[0-9A-Fa-f]{6}$/u.test(metadata.interface.brand_color))) {
160
+ return [finding("plugin.submission.skill.agent.invalid_shape", "Optional agent metadata has an invalid brand color.", { path: agentPath, field: "brand_color" })];
161
+ }
162
+ if (metadata.interface.default_prompt !== undefined && !supportedText(metadata.interface.default_prompt)) {
163
+ return [finding("plugin.submission.skill.agent.invalid_shape", "Optional agent metadata has an invalid default prompt.", { path: agentPath, field: "default_prompt" })];
164
+ }
165
+ if (metadata.policy !== undefined && (!isRecord(metadata.policy) || rejectUnknownKeys(metadata.policy, policyKeys)
166
+ || (metadata.policy.products !== undefined && (!Array.isArray(metadata.policy.products) || metadata.policy.products.length === 0
167
+ || new Set(metadata.policy.products).size !== metadata.policy.products.length || metadata.policy.products.some((product) => product !== "CHAT" && product !== "CODEX")))
168
+ || (metadata.policy.allow_implicit_invocation !== undefined && typeof metadata.policy.allow_implicit_invocation !== "boolean"))) {
169
+ return [finding("plugin.submission.skill.agent.invalid_shape", "Optional agent policy has an unsupported shape.", { path: agentPath, field: "policy" })];
170
+ }
171
+ if (metadata.dependencies !== undefined && (!isRecord(metadata.dependencies) || rejectUnknownKeys(metadata.dependencies, dependencyKeys)
172
+ || !Array.isArray(metadata.dependencies.tools) || metadata.dependencies.tools.length === 0 || metadata.dependencies.tools.some((tool) => !isToolDescriptor(tool)))) {
173
+ return [finding("plugin.submission.skill.agent.invalid_shape", "Optional agent dependencies have an unsupported shape.", { path: agentPath, field: "dependencies" })];
174
+ }
175
+ for (const field of ["icon_small", "icon_large"]) {
176
+ const value = metadata.interface[field];
177
+ if (value === undefined)
178
+ continue;
179
+ const iconFinding = await validateIconPath(reader, skillRoot, skillPath, field, value);
180
+ if (iconFinding !== null)
181
+ return [iconFinding];
182
+ }
183
+ return [];
184
+ }
185
+ export async function validateSubmissionSkillMetadataFromReader(manifest, targetType, reader) {
186
+ if (manifest.skills === undefined) {
187
+ return targetType === "skills-only" ? { findings: [finding("plugin.submission.skill.required", "Skills-only submissions require a valid skill.")], skillCount: 0 } : { findings: [], skillCount: 0 };
188
+ }
189
+ const root = skillsRoot(manifest.skills);
190
+ if (root === null) {
191
+ return { findings: [finding("plugin.submission.skill.invalid_manifest", "Skills must be declared as the root ./skills directory.", { field: "skills" })], skillCount: 0 };
192
+ }
193
+ const rootDetails = await safeStat(reader, root);
194
+ if (rootDetails === null || rootDetails.safeResolution !== "safe" || rootDetails.resolvedKind !== "directory") {
195
+ return { findings: [finding("plugin.submission.skill.invalid_path", "Skills directory must be canonically contained in the package.", { path: root })], skillCount: 0 };
196
+ }
197
+ const listed = await reader.list(root).catch(() => null);
198
+ if (listed === null) {
199
+ return { findings: [finding("plugin.submission.skill.invalid_path", "Skills directory cannot be inspected safely.", { path: root })], skillCount: 0 };
200
+ }
201
+ const findings = [];
202
+ const identities = new Set();
203
+ const budget = { bytes: 0 };
204
+ let skillCount = 0;
205
+ let skillDirectoryCount = 0;
206
+ const entries = [...listed].sort((left, right) => left.packagePath.localeCompare(right.packagePath));
207
+ for (let index = 0; index < entries.length; index += 1) {
208
+ const entry = entries[index];
209
+ const entryCount = index + 1;
210
+ if (entryCount > maxDirectoryEntries) {
211
+ findings.push(finding("plugin.submission.skill.too_many", "Skills directory exceeds the submission preflight entry limit.", { count: entryCount, limit: maxDirectoryEntries }));
212
+ break;
213
+ }
214
+ const name = path.posix.basename(entry.packagePath);
215
+ if (path.posix.dirname(entry.packagePath) !== root || name.startsWith(".") || entry.resolvedKind !== "directory")
216
+ continue;
217
+ if (entry.safeResolution !== "safe" || entry.kind === "symlink") {
218
+ findings.push(finding("plugin.submission.skill.invalid_path", "Skill directory resolves outside the declared skills directory.", { path: entry.packagePath }));
219
+ continue;
220
+ }
221
+ skillDirectoryCount += 1;
222
+ if (skillDirectoryCount > maxSkillDirectories) {
223
+ findings.push(finding("plugin.submission.skill.too_many", "Skills directory exceeds the submission preflight skill limit.", { count: skillDirectoryCount, limit: maxSkillDirectories }));
224
+ break;
225
+ }
226
+ const skillRoot = entry.packagePath;
227
+ const skillFile = `${skillRoot}/SKILL.md`;
228
+ const fileDetails = await safeStat(reader, skillFile);
229
+ if (fileDetails?.kind === "symlink" || fileDetails?.safeResolution === "outside") {
230
+ findings.push(finding("plugin.submission.skill.invalid_path", "Skill entrypoint must not be a symbolic link.", { path: skillFile }));
231
+ continue;
232
+ }
233
+ if (fileDetails === null || fileDetails.safeResolution !== "safe" || fileDetails.resolvedKind !== "file") {
234
+ findings.push(finding("plugin.submission.skill.invalid_file", "Skill entrypoint must be a contained regular file.", { path: skillFile }));
235
+ continue;
236
+ }
237
+ const source = await readSafeUtf8(reader, skillFile, maxSkillBytes, budget);
238
+ if (source.kind === "budget") {
239
+ findings.push(budgetFinding(source.nextBytes));
240
+ break;
241
+ }
242
+ if (source.kind === "invalid") {
243
+ findings.push(finding("plugin.submission.skill.invalid_file", "Skill entrypoint must be a bounded UTF-8 regular file.", { path: skillFile, limit: maxSkillBytes }));
244
+ continue;
245
+ }
246
+ const split = splitSkillFile(source.source);
247
+ if (split === null) {
248
+ findings.push(finding("plugin.submission.skill.invalid_file", "Skill entrypoint requires delimited frontmatter and a nonempty body.", { path: skillFile }));
249
+ continue;
250
+ }
251
+ const parsed = parseSafeYaml(split.frontmatter);
252
+ if ("error" in parsed) {
253
+ findings.push(finding(parsed.error === "yaml" ? "plugin.submission.skill.invalid_yaml" : "plugin.submission.skill.invalid_shape", "Skill frontmatter must be a safe YAML mapping.", { path: skillFile }));
254
+ continue;
255
+ }
256
+ const nameValue = parsed.value.name;
257
+ const description = parsed.value.description;
258
+ const normalizedName = typeof nameValue === "string" ? nameValue.normalize("NFKC").trim() : "";
259
+ const pluginName = typeof manifest.name === "string" ? manifest.name.normalize("NFKC").trim() : "";
260
+ if (!supportedText(nameValue) || !supportedText(description, 1024) || normalizedName.length === 0 || `${pluginName}:${normalizedName}`.length > 64 || identities.has(normalizedName)) {
261
+ findings.push(finding("plugin.submission.skill.identity", "Skill identity metadata is invalid or duplicated.", { path: skillFile, limit: 64 }));
262
+ continue;
263
+ }
264
+ identities.add(normalizedName);
265
+ skillCount += 1;
266
+ const resolvedSkillRoot = entry.resolvedPackagePath ?? skillRoot;
267
+ const agentFindings = await validateAgentFile(reader, skillRoot, resolvedSkillRoot, budget);
268
+ findings.push(...agentFindings);
269
+ if (agentFindings.some((item) => item.id === "plugin.submission.skill.budget_exceeded"))
270
+ break;
271
+ }
272
+ if (targetType === "skills-only" && skillCount === 0) {
273
+ findings.push(finding("plugin.submission.skill.required", "Skills-only submissions require at least one valid skill.", { count: 0 }));
274
+ }
275
+ if (targetType === "mcp-backed" && skillCount === 0 && findings.length === 0) {
276
+ findings.push(finding("plugin.submission.skill.invalid_file", "Declared skills must include at least one valid immediate skill entrypoint.", { path: root }));
277
+ }
278
+ return { findings, skillCount };
279
+ }
280
+ export async function validateSubmissionSkillMetadata(discoveredPackage, targetType) {
281
+ return validateSubmissionSkillMetadataFromReader(discoveredPackage.manifest, targetType, createDirectorySubmissionPackageReader(discoveredPackage.rootPath));
282
+ }
package/dist/index.d.ts CHANGED
@@ -14,6 +14,13 @@ export { buildDoctorAttestation, renderDoctorAttestation, renderDoctorAttestatio
14
14
  export { buildExternalValidationCorpusReport, ExternalCorpusManifestError, loadExternalCorpusManifest, type BuildExternalValidationCorpusOptions, type ExternalCorpusCaseResult, type ExternalCorpusClassificationCounts, type ExternalCorpusFindingReview, type ExternalCorpusManifest, type ExternalCorpusMode, type ExternalCorpusProfile, type ExternalCorpusSourceType, type ExternalValidationCorpusReport, type FindingReviewClassification } from "./core/external-validation-corpus.js";
15
15
  export { buildCorpusQualityMetricsDiffReport, buildCorpusQualityMetricsReport, CorpusMetricsDiffError, CorpusMetricsManifestError, loadCorpusMetricsManifest, reconcileCorpusMetricFindings, renderCorpusQualityMetricsDiffJson, renderCorpusQualityMetricsDiffMarkdown, renderCorpusQualityMetricsDiffText, renderCorpusQualityMetricsJson, renderCorpusQualityMetricsMarkdown, renderCorpusQualityMetricsText, type BuildCorpusMetricsDiffOptions, type BuildCorpusMetricsOptions, type CorpusMetricCounts, type CorpusMetricCountDeltas, type CorpusMetricFinding, type CorpusMetricFindingReview, type CorpusMetricReconciliation, type CorpusMetricsManifest, type CorpusMetricSource, type CorpusMetricThresholdCheck, type CorpusMetricThresholds, type CorpusMetricValues, type CorpusMetricValueComparison, type CorpusQualityMetricsDiffReport, type CorpusQualityMetricsDiffTarget, type CorpusQualityMetricsReport, type CorpusQualityMetricsTargetResult } from "./core/corpus-quality-metrics.js";
16
16
  export { buildDoctorOutputContract, renderDoctorOutputContract, renderDoctorOutputContractJson, type DoctorOutputContract, type OutputContractRule, type OutputContractSchema } from "./core/output-contract.js";
17
+ export { submissionRuleset, submissionManualChecks } from "./core/submission-ruleset.js";
18
+ export { buildSubmissionPreflight, type SubmissionCheck, type SubmissionFinding, type SubmissionManualCheck, type SubmissionPreflightReport } from "./core/submission-preflight.js";
19
+ export { buildSubmissionArchivePreflight, submissionArchiveExitCode, type SubmissionArchivePreflightReport, type SubmissionArchiveRootLayout } from "./core/submission-archive-preflight.js";
20
+ export { validateSubmissionAssets, type SubmissionAssetResult } from "./core/submission-assets.js";
21
+ export { validateSubmissionSkillMetadata, type SubmissionSkillMetadataResult } from "./core/submission-skill-metadata.js";
22
+ export { renderSubmissionPreflightJson, renderSubmissionPreflightMarkdown, renderSubmissionPreflightText, submissionPreflightExitCode } from "./reporting/render-submission-report.js";
23
+ export { renderSubmissionArchiveJson, renderSubmissionArchiveMarkdown, renderSubmissionArchiveText } from "./reporting/render-submission-archive-report.js";
17
24
  export { buildDoctorValidationCorpusReport, renderDoctorValidationCorpusJson, renderDoctorValidationCorpusReport, type BuildDoctorValidationCorpusOptions, type DoctorValidationCorpusReport, type ValidationCorpusCaseDefinition, type ValidationCorpusCaseResult } from "./core/validation-corpus.js";
18
25
  export { buildDoctorExportBundleFromAnalysis, buildDoctorRecommendationsFromAnalysis, buildPackageAnalysis, type PackageAnalysis, type PackageAnalysisOptions, type PackageAnalysisStage, type PackageAnalysisTiming } from "./core/package-analysis.js";
19
26
  export { buildDoctorPerformanceReport, renderDoctorPerformanceReport, renderDoctorPerformanceReportJson, type BuildDoctorPerformanceReportOptions, type DoctorPerformanceReport, type DoctorPerformanceStage, type DoctorPerformanceStageName } from "./core/performance-report.js";
package/dist/index.js CHANGED
@@ -14,6 +14,13 @@ export { buildDoctorAttestation, renderDoctorAttestation, renderDoctorAttestatio
14
14
  export { buildExternalValidationCorpusReport, ExternalCorpusManifestError, loadExternalCorpusManifest } from "./core/external-validation-corpus.js";
15
15
  export { buildCorpusQualityMetricsDiffReport, buildCorpusQualityMetricsReport, CorpusMetricsDiffError, CorpusMetricsManifestError, loadCorpusMetricsManifest, reconcileCorpusMetricFindings, renderCorpusQualityMetricsDiffJson, renderCorpusQualityMetricsDiffMarkdown, renderCorpusQualityMetricsDiffText, renderCorpusQualityMetricsJson, renderCorpusQualityMetricsMarkdown, renderCorpusQualityMetricsText } from "./core/corpus-quality-metrics.js";
16
16
  export { buildDoctorOutputContract, renderDoctorOutputContract, renderDoctorOutputContractJson } from "./core/output-contract.js";
17
+ export { submissionRuleset, submissionManualChecks } from "./core/submission-ruleset.js";
18
+ export { buildSubmissionPreflight } from "./core/submission-preflight.js";
19
+ export { buildSubmissionArchivePreflight, submissionArchiveExitCode } from "./core/submission-archive-preflight.js";
20
+ export { validateSubmissionAssets } from "./core/submission-assets.js";
21
+ export { validateSubmissionSkillMetadata } from "./core/submission-skill-metadata.js";
22
+ export { renderSubmissionPreflightJson, renderSubmissionPreflightMarkdown, renderSubmissionPreflightText, submissionPreflightExitCode } from "./reporting/render-submission-report.js";
23
+ export { renderSubmissionArchiveJson, renderSubmissionArchiveMarkdown, renderSubmissionArchiveText } from "./reporting/render-submission-archive-report.js";
17
24
  export { buildDoctorValidationCorpusReport, renderDoctorValidationCorpusJson, renderDoctorValidationCorpusReport } from "./core/validation-corpus.js";
18
25
  export { buildDoctorExportBundleFromAnalysis, buildDoctorRecommendationsFromAnalysis, buildPackageAnalysis } from "./core/package-analysis.js";
19
26
  export { buildDoctorPerformanceReport, renderDoctorPerformanceReport, renderDoctorPerformanceReportJson } from "./core/performance-report.js";
@@ -0,0 +1,4 @@
1
+ import type { SubmissionArchivePreflightReport } from "../core/submission-archive-preflight.js";
2
+ export declare function renderSubmissionArchiveJson(report: SubmissionArchivePreflightReport): string;
3
+ export declare function renderSubmissionArchiveText(report: SubmissionArchivePreflightReport): string;
4
+ export declare function renderSubmissionArchiveMarkdown(report: SubmissionArchivePreflightReport): string;
@@ -0,0 +1,52 @@
1
+ function upper(value) {
2
+ return value.replace(/[-_]/gu, " ").toUpperCase();
3
+ }
4
+ function escapeMarkdown(value) {
5
+ return value.replace(/[\\`*_{}\[\]()<>#+\-.!|]/gu, "\\$&");
6
+ }
7
+ function summary(report) {
8
+ return [
9
+ `Ruleset: ${report.rulesetVersion}`,
10
+ `Archive: ${report.archive.fileName}`,
11
+ `Root layout: ${report.archive.rootLayout}`,
12
+ `Automatic status: ${upper(report.status)}`,
13
+ `Readiness: ${upper(report.readiness)}`,
14
+ `Summary: ${report.summary.passed} passed, ${report.summary.warnings} warnings, ${report.summary.blockers} blockers, ${report.summary.manualChecks} manual checks`,
15
+ "Manual review is required; automatic checks do not complete archive review."
16
+ ];
17
+ }
18
+ export function renderSubmissionArchiveJson(report) {
19
+ return `${JSON.stringify(report, null, 2)}\n`;
20
+ }
21
+ export function renderSubmissionArchiveText(report) {
22
+ const lines = ["Submission archive preflight", "============================", ...summary(report), "", "Findings"];
23
+ lines.push(...(report.findings.length === 0
24
+ ? [" None"]
25
+ : report.findings.map((finding) => ` ${upper(finding.severity)} ${finding.id}: ${finding.message}`)));
26
+ lines.push("", "Coverage");
27
+ lines.push(...(report.coverage.length === 0
28
+ ? [" None"]
29
+ : report.coverage.map((item) => ` ${upper(item.status)} ${item.id}: ${item.reason}`)));
30
+ lines.push("", "Manual checklist");
31
+ lines.push(...report.manualChecklist.map((item) => ` ${upper(item.state)} ${item.id}: ${item.label}`));
32
+ return `${lines.join("\n")}\n`;
33
+ }
34
+ export function renderSubmissionArchiveMarkdown(report) {
35
+ const lines = [
36
+ "# Submission archive preflight",
37
+ "",
38
+ ...summary(report).map((item) => `- ${escapeMarkdown(item)}`),
39
+ "",
40
+ "## Findings"
41
+ ];
42
+ lines.push(...(report.findings.length === 0
43
+ ? ["- None"]
44
+ : report.findings.map((finding) => `- **${escapeMarkdown(upper(finding.severity))}** ${escapeMarkdown(finding.id)}: ${escapeMarkdown(finding.message)}`)));
45
+ lines.push("", "## Coverage");
46
+ lines.push(...(report.coverage.length === 0
47
+ ? ["- None"]
48
+ : report.coverage.map((item) => `- ${escapeMarkdown(upper(item.status))}: ${escapeMarkdown(item.id)} — ${escapeMarkdown(item.reason)}`)));
49
+ lines.push("", "## Manual checklist");
50
+ lines.push(...report.manualChecklist.map((item) => `- ${escapeMarkdown(upper(item.state))}: ${escapeMarkdown(item.id)} — ${escapeMarkdown(item.label)}`));
51
+ return `${lines.join("\n")}\n`;
52
+ }
@@ -0,0 +1,5 @@
1
+ import type { SubmissionPreflightReport } from "../core/submission-preflight.js";
2
+ export declare function renderSubmissionPreflightJson(report: SubmissionPreflightReport): string;
3
+ export declare function renderSubmissionPreflightText(report: SubmissionPreflightReport): string;
4
+ export declare function renderSubmissionPreflightMarkdown(report: SubmissionPreflightReport): string;
5
+ export declare function submissionPreflightExitCode(report: SubmissionPreflightReport, requireReady: boolean): 0 | 1;
@@ -0,0 +1,68 @@
1
+ function upper(value) {
2
+ return value.replace(/[-_]/gu, " ").toUpperCase();
3
+ }
4
+ function findingsByCheck(report) {
5
+ const findings = new Map(report.findings.map((finding) => [finding.id, finding]));
6
+ return report.checks.map((check) => ({
7
+ check,
8
+ findings: check.findingIds.flatMap((id) => {
9
+ const finding = findings.get(id);
10
+ return finding ? [finding] : [];
11
+ })
12
+ }));
13
+ }
14
+ function escapeMarkdown(value) {
15
+ return value.replace(/[\\`*_{}\[\]()<>#+\-.!|]/gu, "\\$&");
16
+ }
17
+ function textSummary(report) {
18
+ return [
19
+ `Ruleset: ${report.rulesetVersion}`,
20
+ `Target: ${report.targetType}`,
21
+ `Automatic status: ${upper(report.status)}`,
22
+ `Readiness: ${upper(report.readiness)}`,
23
+ `Summary: ${report.summary.passed} passed, ${report.summary.warnings} warnings, ${report.summary.blockers} blockers, ${report.summary.manualChecks} manual checks`,
24
+ "Manual review is required; automatic checks do not complete directory review."
25
+ ];
26
+ }
27
+ export function renderSubmissionPreflightJson(report) {
28
+ return `${JSON.stringify(report, null, 2)}\n`;
29
+ }
30
+ export function renderSubmissionPreflightText(report) {
31
+ const lines = ["Submission preflight", "====================", ...textSummary(report), "", "Findings by check"];
32
+ for (const { check, findings } of findingsByCheck(report)) {
33
+ lines.push(`${check.id} (${upper(check.status)})`);
34
+ lines.push(...(findings.length === 0
35
+ ? [" None"]
36
+ : findings.map((finding) => ` ${upper(finding.severity)} ${finding.id}: ${finding.message}`)));
37
+ }
38
+ lines.push("", "Manual checklist");
39
+ lines.push(...report.manualChecklist.map((item) => ` ${upper(item.state)} ${item.id}: ${item.label}`));
40
+ return `${lines.join("\n")}\n`;
41
+ }
42
+ export function renderSubmissionPreflightMarkdown(report) {
43
+ const lines = [
44
+ "# Submission preflight",
45
+ "",
46
+ `- Ruleset: ${escapeMarkdown(report.rulesetVersion)}`,
47
+ `- Target: ${escapeMarkdown(report.targetType)}`,
48
+ `- Automatic status: ${escapeMarkdown(upper(report.status))}`,
49
+ `- Readiness: ${escapeMarkdown(upper(report.readiness))}`,
50
+ `- Summary: ${report.summary.passed} passed, ${report.summary.warnings} warnings, ${report.summary.blockers} blockers, ${report.summary.manualChecks} manual checks`,
51
+ "",
52
+ "Manual review is required; automatic checks do not complete directory review.",
53
+ "",
54
+ "## Findings by check"
55
+ ];
56
+ for (const { check, findings } of findingsByCheck(report)) {
57
+ lines.push("", `### ${escapeMarkdown(check.id)} (${escapeMarkdown(upper(check.status))})`);
58
+ lines.push(...(findings.length === 0
59
+ ? ["- None"]
60
+ : findings.map((finding) => `- **${escapeMarkdown(upper(finding.severity))}** ${escapeMarkdown(finding.id)}: ${escapeMarkdown(finding.message)}`)));
61
+ }
62
+ lines.push("", "## Manual checklist");
63
+ lines.push(...report.manualChecklist.map((item) => `- ${escapeMarkdown(upper(item.state))}: ${escapeMarkdown(item.id)} — ${escapeMarkdown(item.label)}`));
64
+ return `${lines.join("\n")}\n`;
65
+ }
66
+ export function submissionPreflightExitCode(report, requireReady) {
67
+ return requireReady && report.status === "fail" ? 1 : 0;
68
+ }