@jay-framework/jay-stack-cli 0.25.1 → 0.25.2

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/dist/index.d.ts CHANGED
@@ -1,6 +1,15 @@
1
1
  import { LogLevel } from '@jay-framework/logger';
2
2
  export { MaterializeContractsOptions, MaterializeResult, PluginContractEntry, PluginsIndex, PluginsIndexEntry, listContracts, materializeContracts } from '@jay-framework/stack-server-build';
3
3
 
4
+ /**
5
+ * `express.static` `setHeaders` callback that mirrors production for agent-readiness resources
6
+ * (DL#217): everything under `/.well-known/` is served CORS-open, with `SKILL.md` as
7
+ * `text/markdown` and skill archives (`.gz`) as `application/gzip`. Exported so the dev-serving
8
+ * test guards the real behavior rather than a copy.
9
+ */
10
+ declare function setWellKnownHeaders(res: {
11
+ setHeader(name: string, value: string): void;
12
+ }, filePath: string): void;
4
13
  interface StartDevServerOptions {
5
14
  projectPath?: string;
6
15
  testMode?: boolean;
@@ -19,10 +28,34 @@ interface JayConfig {
19
28
  };
20
29
  site?: {
21
30
  baseUrl?: string;
31
+ /** Site/organization name — drives Organization + WebSite JSON-LD (DL#213). */
32
+ name?: string;
33
+ /** Logo URL (absolute or baseUrl-relative) for the Organization JSON-LD (DL#213). */
34
+ logo?: string;
35
+ /** Short site description for the Organization JSON-LD (DL#213). */
36
+ description?: string;
37
+ /** Social/profile URLs for the Organization `sameAs` (DL#213). */
38
+ sameAs?: string[];
39
+ };
40
+ /** Project-wide validation suppressions for project-level concerns (DL#214). */
41
+ validations?: {
42
+ /**
43
+ * Suppress the `/.well-known/ard.json` absence nudge — for sites with no agent-callable
44
+ * resources/APIs to advertise. Lives in `.jay` (not a page's `application/jay-validations`
45
+ * script) because it governs a single project-level file, not any page's markup.
46
+ */
47
+ 'allow-no-ard'?: boolean;
48
+ /**
49
+ * Suppress the minimum-error-pages nudge — for sites that intentionally ship no branded
50
+ * `404`/`5xx` pages and accept the plain-text fallback. Lives in `.jay` (not a page's
51
+ * `application/jay-validations` script) because it governs the project's `pages/` tree as a
52
+ * whole, not any single page's markup (DL#218).
53
+ */
54
+ 'allow-no-error-pages'?: boolean;
22
55
  };
23
56
  }
24
57
  declare function loadConfig(projectRoot?: string): JayConfig;
25
58
  declare function getConfigWithDefaults(config: JayConfig): Required<Pick<JayConfig, 'devServer'>> & Pick<JayConfig, 'site'>;
26
59
  declare function updateConfig(updates: Partial<JayConfig>): void;
27
60
 
28
- export { type JayConfig, type StartDevServerOptions, getConfigWithDefaults, loadConfig, startDevServer, updateConfig };
61
+ export { type JayConfig, type StartDevServerOptions, getConfigWithDefaults, loadConfig, setWellKnownHeaders, startDevServer, updateConfig };
package/dist/index.js CHANGED
@@ -50,7 +50,8 @@ function loadConfig(projectRoot) {
50
50
  ...DEFAULT_CONFIG.devServer,
51
51
  ...userConfig.devServer
52
52
  },
53
- site: userConfig.site
53
+ site: userConfig.site,
54
+ validations: userConfig.validations
54
55
  };
55
56
  } catch (error) {
56
57
  getLogger().warn(`Failed to parse .jay YAML config file, using defaults: ${error}`);
@@ -199,6 +200,13 @@ const generateContractDefinitionFiles$1 = /* @__PURE__ */ Object.freeze(/* @__PU
199
200
  __proto__: null,
200
201
  generateContractDefinitionFiles
201
202
  }, Symbol.toStringTag, { value: "Module" }));
203
+ function setWellKnownHeaders(res, filePath) {
204
+ if (filePath.includes(`${path$1.sep}.well-known${path$1.sep}`)) {
205
+ res.setHeader("Access-Control-Allow-Origin", "*");
206
+ if (filePath.endsWith(".md")) res.setHeader("Content-Type", "text/markdown; charset=utf-8");
207
+ if (filePath.endsWith(".gz")) res.setHeader("Content-Type", "application/gzip");
208
+ }
209
+ }
202
210
  async function startDevServer(options = {}) {
203
211
  const projectPath = options.projectPath || process.cwd();
204
212
  if (projectPath !== process.cwd()) {
@@ -214,7 +222,7 @@ async function startDevServer(options = {}) {
214
222
  const httpServer = http.createServer(app);
215
223
  const devServerPort = await getPort({ port: resolvedConfig.devServer.portRange });
216
224
  const log = getLogger();
217
- const { server, viteServer, routes, service } = await mkDevServer({
225
+ const { server, viteServer, routes, service, errorPageHandler } = await mkDevServer({
218
226
  pagesRootFolder: path$1.resolve(resolvedConfig.devServer.pagesBase),
219
227
  projectRootFolder: process.cwd(),
220
228
  publicBaseUrlPath: "/",
@@ -232,18 +240,20 @@ async function startDevServer(options = {}) {
232
240
  });
233
241
  }
234
242
  if (fs$1.existsSync(publicPath)) {
235
- app.use(express.static(publicPath));
243
+ app.use(express.static(publicPath, { dotfiles: "allow", setHeaders: setWellKnownHeaders }));
236
244
  } else {
237
245
  log.important(`⚠️ Public folder not found: ${resolvedConfig.devServer.publicFolder}`);
238
246
  }
247
+ const pageRouter = express.Router();
239
248
  routes.forEach((route) => {
240
- app.get(route.path, route.handler);
249
+ pageRouter.get(route.path, route.handler);
241
250
  });
242
251
  service.attachRouteRegistrar((added) => {
243
252
  added.forEach((route) => {
244
- app.get(route.path, route.handler);
253
+ pageRouter.get(route.path, route.handler);
245
254
  });
246
255
  });
256
+ app.use(pageRouter);
247
257
  void (async () => {
248
258
  await generatePageDefinitionFiles(
249
259
  routes.map((route) => route.fsRoute.jayHtmlPath),
@@ -298,6 +308,7 @@ async function startDevServer(options = {}) {
298
308
  }, options.timeout * 1e3);
299
309
  }
300
310
  }
311
+ app.use(errorPageHandler(404));
301
312
  process.on("SIGTERM", shutdown);
302
313
  process.on("SIGINT", shutdown);
303
314
  }
@@ -3004,6 +3015,61 @@ async function validateAiditorSettings(context, result) {
3004
3015
  });
3005
3016
  }
3006
3017
  }
3018
+ async function compileValidateRouteJayHtml(absoluteJayHtmlPath, pluginRoot) {
3019
+ const content = fs$1.readFileSync(absoluteJayHtmlPath, "utf-8");
3020
+ const dirname = path$1.dirname(absoluteJayHtmlPath);
3021
+ const filename = path$1.basename(absoluteJayHtmlPath, JAY_EXTENSION$1);
3022
+ const errors = [];
3023
+ try {
3024
+ const parsedFile = await parseJayFile(
3025
+ content,
3026
+ filename,
3027
+ dirname,
3028
+ {},
3029
+ JAY_IMPORT_RESOLVER,
3030
+ pluginRoot
3031
+ );
3032
+ for (const validation of parsedFile.validations) {
3033
+ errors.push(validation);
3034
+ }
3035
+ if (!parsedFile.val || parsedFile.validations.length > 0) {
3036
+ return errors;
3037
+ }
3038
+ const generatedFile = generateElementFile(parsedFile.val, RuntimeMode$1.MainTrusted);
3039
+ for (const validation of generatedFile.validations) {
3040
+ errors.push(validation);
3041
+ }
3042
+ const serverElementFile = generateServerElementFile(parsedFile.val);
3043
+ for (const validation of serverElementFile.validations) {
3044
+ errors.push(`[SSR] ${validation}`);
3045
+ }
3046
+ } catch (error) {
3047
+ const message = error instanceof Error ? error.message : String(error);
3048
+ errors.push(message);
3049
+ }
3050
+ return errors;
3051
+ }
3052
+ async function validateRouteJayHtmlTemplates(context, result) {
3053
+ const routes = context.manifest.routes;
3054
+ if (!routes || !Array.isArray(routes)) return;
3055
+ for (const route of routes) {
3056
+ if (!route.jayHtml) continue;
3057
+ const absolutePath = path$1.resolve(context.pluginPath, route.jayHtml);
3058
+ if (!fs$1.existsSync(absolutePath)) {
3059
+ continue;
3060
+ }
3061
+ const compileErrors = await compileValidateRouteJayHtml(absolutePath, context.pluginPath);
3062
+ const relPath = path$1.relative(context.pluginPath, absolutePath);
3063
+ for (const message of compileErrors) {
3064
+ result.errors.push({
3065
+ type: "jay-html-compile",
3066
+ message: `Route "${route.path}" jayHtml failed to compile: ${message}`,
3067
+ location: relPath,
3068
+ suggestion: "Fix the template syntax or run `jay-stack validate -p <dir>` on the source page during development. See agent-kit/designer/jay-html-template-syntax.md"
3069
+ });
3070
+ }
3071
+ }
3072
+ }
3007
3073
  async function validatePlugin(options = {}) {
3008
3074
  const pluginPath = options.pluginPath || process.cwd();
3009
3075
  if (options.local) {
@@ -3073,6 +3139,7 @@ async function validatePluginPackage(pluginPath, options) {
3073
3139
  await validateAddMenuCatalog(context, result);
3074
3140
  await validateAiditorSettings(context, result);
3075
3141
  validateNoCompilerLeak(context, result);
3142
+ await validateRouteJayHtmlTemplates(context, result);
3076
3143
  result.valid = result.errors.length === 0;
3077
3144
  return result;
3078
3145
  }
@@ -3927,6 +3994,20 @@ async function validatePackageJson(context, result) {
3927
3994
  suggestion: 'Add "./plugin.yaml": "./plugin.yaml" to exports field'
3928
3995
  });
3929
3996
  }
3997
+ if (context.manifest.commands) {
3998
+ for (const cmd of context.manifest.commands) {
3999
+ if (!cmd.command) continue;
4000
+ const exportKey = `./${cmd.command}`;
4001
+ if (!packageJson.exports || !packageJson.exports[exportKey]) {
4002
+ result.errors.push({
4003
+ type: "export-mismatch",
4004
+ message: `command metadata file "${cmd.command}" not exported in package.json (required for plugin resolution)`,
4005
+ location: packageJsonPath,
4006
+ suggestion: `Add "${exportKey}": "${exportKey}" to exports field`
4007
+ });
4008
+ }
4009
+ }
4010
+ }
3930
4011
  const agentKitDir = path$1.join(context.pluginPath, "agent-kit");
3931
4012
  if (fs$1.existsSync(agentKitDir) && fs$1.statSync(agentKitDir).isDirectory()) {
3932
4013
  const filesArray = packageJson.files;
@@ -4789,10 +4870,10 @@ function routeToRegex(route) {
4789
4870
  async function buildRouteOracle(pagesBase) {
4790
4871
  let routes = [];
4791
4872
  try {
4792
- routes = await scanRoutes(pagesBase, {
4873
+ ({ routes } = await scanRoutes(pagesBase, {
4793
4874
  jayHtmlFilename: "page.jay-html",
4794
4875
  compFilename: "page.ts"
4795
- });
4876
+ }));
4796
4877
  } catch {
4797
4878
  routes = [];
4798
4879
  }
@@ -5005,6 +5086,89 @@ async function loadSlowViewState(cacheAbsPath) {
5005
5086
  return {};
5006
5087
  }
5007
5088
  }
5089
+ async function loadCarryForward(cacheAbsPath) {
5090
+ try {
5091
+ const cache = JSON.parse(await promises.readFile(cacheAbsPath, "utf8"));
5092
+ return cache.carryForward ?? {};
5093
+ } catch {
5094
+ return {};
5095
+ }
5096
+ }
5097
+ function jsonLdBodyError(body) {
5098
+ const text = body?.trim();
5099
+ if (!text) return "Empty JSON-LD block — add structured data or remove the tag.";
5100
+ let parsed;
5101
+ try {
5102
+ parsed = JSON.parse(text);
5103
+ } catch (e2) {
5104
+ return `Invalid JSON: ${e2.message}`;
5105
+ }
5106
+ const objs = Array.isArray(parsed) ? parsed : [parsed];
5107
+ for (const obj of objs) {
5108
+ if (!obj || typeof obj !== "object") {
5109
+ return "JSON-LD must be an object (or array of objects) with @context and @type.";
5110
+ }
5111
+ if (!("@context" in obj) || !("@type" in obj)) {
5112
+ return "JSON-LD is missing @context or @type.";
5113
+ }
5114
+ }
5115
+ return null;
5116
+ }
5117
+ function isLdJsonHeadTag(tag) {
5118
+ return tag?.tag?.toLowerCase?.() === "script" && tag?.attrs?.type === "application/ld+json";
5119
+ }
5120
+ function checkBuildJsonLd(manifest) {
5121
+ const findings = [];
5122
+ for (const tag of manifest.siteHeadTags ?? []) {
5123
+ if (!isLdJsonHeadTag(tag)) continue;
5124
+ const err = jsonLdBodyError(tag.children);
5125
+ if (err) {
5126
+ findings.push({
5127
+ file: ".jay (site.*)",
5128
+ message: `Site JSON-LD: ${err}`,
5129
+ severity: "error",
5130
+ source: "build-output"
5131
+ });
5132
+ }
5133
+ }
5134
+ for (const route of manifest.routes) {
5135
+ for (const script of route.scripts ?? []) {
5136
+ if (script.attributes?.type !== "application/ld+json") continue;
5137
+ const err = jsonLdBodyError(script.inline);
5138
+ if (err) {
5139
+ findings.push({
5140
+ file: route.pattern,
5141
+ message: `Template JSON-LD: ${err}`,
5142
+ severity: "error",
5143
+ source: "build-output"
5144
+ });
5145
+ }
5146
+ }
5147
+ }
5148
+ return findings;
5149
+ }
5150
+ function checkInstanceJsonLd(args) {
5151
+ const { instanceUrl, carryForward } = args;
5152
+ const findings = [];
5153
+ const slowHeadTags = carryForward?.__slowHeadTags;
5154
+ if (!Array.isArray(slowHeadTags)) return findings;
5155
+ for (const source of slowHeadTags) {
5156
+ if (!Array.isArray(source)) continue;
5157
+ for (const tag of source) {
5158
+ if (!isLdJsonHeadTag(tag)) continue;
5159
+ const err = jsonLdBodyError(tag?.children);
5160
+ if (err) {
5161
+ findings.push({
5162
+ file: instanceUrl,
5163
+ message: `Component JSON-LD: ${err}`,
5164
+ severity: "error",
5165
+ source: "build-output"
5166
+ });
5167
+ }
5168
+ }
5169
+ }
5170
+ return findings;
5171
+ }
5008
5172
  function checkTemplateLinksAgainstBuild(args) {
5009
5173
  const { parsedFiles, buildUrls, assetUrls, dynamicMatchers, baseUrl } = args;
5010
5174
  const findings = [];
@@ -5168,6 +5332,122 @@ async function newestSourceMtime(dirs) {
5168
5332
  for (const dir of dirs) await walk(dir);
5169
5333
  return newest;
5170
5334
  }
5335
+ const URN_PATTERN = /^urn:air:[^:\s]+:[^:\s]+:[^:\s]+$/;
5336
+ const MEDIA_TYPE_PATTERN = /^[a-z]+\/[A-Za-z0-9.+-]+$/;
5337
+ const ARD_SUGGEST = "See developer/seo-guide.md (ARD v0.91) for the schema and a worked example.";
5338
+ function validateArdDocument(doc) {
5339
+ const findings = [];
5340
+ if (!doc || typeof doc !== "object") {
5341
+ return [
5342
+ {
5343
+ message: "ard.json must be a JSON object with an `entries` array.",
5344
+ severity: "error",
5345
+ suggestion: ARD_SUGGEST
5346
+ }
5347
+ ];
5348
+ }
5349
+ const entries = doc.entries;
5350
+ if (!Array.isArray(entries)) {
5351
+ return [
5352
+ {
5353
+ message: "ard.json is missing the required `entries` array.",
5354
+ severity: "error",
5355
+ suggestion: ARD_SUGGEST
5356
+ }
5357
+ ];
5358
+ }
5359
+ entries.forEach((entry, i) => {
5360
+ const at = `ard.json entries[${i}]`;
5361
+ if (!entry || typeof entry !== "object") {
5362
+ findings.push({ message: `${at} must be an object.`, severity: "error" });
5363
+ return;
5364
+ }
5365
+ if (typeof entry.identifier !== "string" || !URN_PATTERN.test(entry.identifier)) {
5366
+ findings.push({
5367
+ message: `${at}.identifier must match urn:air:<publisher>:<namespace>:<name>.`,
5368
+ severity: "error",
5369
+ suggestion: ARD_SUGGEST
5370
+ });
5371
+ }
5372
+ if (typeof entry.displayName !== "string" || entry.displayName.trim() === "") {
5373
+ findings.push({
5374
+ message: `${at}.displayName is required.`,
5375
+ severity: "error"
5376
+ });
5377
+ }
5378
+ if (typeof entry.type !== "string" || !MEDIA_TYPE_PATTERN.test(entry.type)) {
5379
+ findings.push({
5380
+ message: `${at}.type must be an IANA media type (e.g. application/mcp-server-card+json).`,
5381
+ severity: "error"
5382
+ });
5383
+ }
5384
+ const hasUrl = entry.url !== void 0;
5385
+ const hasData = entry.data !== void 0;
5386
+ if (hasUrl === hasData) {
5387
+ findings.push({
5388
+ message: `${at} must have exactly one of \`url\` or \`data\`.`,
5389
+ severity: "error"
5390
+ });
5391
+ }
5392
+ const rq = entry.representativeQueries;
5393
+ if (rq === void 0) {
5394
+ findings.push({
5395
+ message: `${at} is missing representativeQueries (2–5 recommended) — agents may not surface this resource.`,
5396
+ severity: "warning",
5397
+ suggestion: ARD_SUGGEST
5398
+ });
5399
+ } else if (!Array.isArray(rq) || rq.length < 2 || rq.length > 5) {
5400
+ findings.push({
5401
+ message: `${at}.representativeQueries should list 2–5 example queries.`,
5402
+ severity: "warning"
5403
+ });
5404
+ }
5405
+ });
5406
+ return findings;
5407
+ }
5408
+ async function checkArdFiles(projectRoot) {
5409
+ const wellKnown = path$1.resolve(projectRoot, "public/.well-known");
5410
+ const ardPath = path$1.join(wellKnown, "ard.json");
5411
+ const aliasPath = path$1.join(wellKnown, "ai-catalog.json");
5412
+ const ardRaw = await readIfExists(ardPath);
5413
+ const aliasRaw = await readIfExists(aliasPath);
5414
+ if (ardRaw === null && aliasRaw === null) {
5415
+ return { findings: [], absent: true };
5416
+ }
5417
+ const findings = [];
5418
+ if (ardRaw === null && aliasRaw !== null) {
5419
+ findings.push({
5420
+ message: "Only public/.well-known/ai-catalog.json found — ARD renamed this to ard.json. Add ard.json (keep ai-catalog.json as a copy for older clients).",
5421
+ severity: "warning",
5422
+ suggestion: ARD_SUGGEST
5423
+ });
5424
+ } else if (ardRaw !== null && aliasRaw !== null && ardRaw.trim() !== aliasRaw.trim()) {
5425
+ findings.push({
5426
+ message: "public/.well-known/ard.json and ai-catalog.json differ — the alias should be a copy of ard.json.",
5427
+ severity: "warning"
5428
+ });
5429
+ }
5430
+ const canonicalRaw = ardRaw ?? aliasRaw;
5431
+ let parsed;
5432
+ try {
5433
+ parsed = JSON.parse(canonicalRaw);
5434
+ } catch (e2) {
5435
+ findings.push({
5436
+ message: `${ardRaw !== null ? "ard.json" : "ai-catalog.json"} is not valid JSON: ${e2.message}`,
5437
+ severity: "error"
5438
+ });
5439
+ return { findings, absent: false };
5440
+ }
5441
+ findings.push(...validateArdDocument(parsed));
5442
+ return { findings, absent: false };
5443
+ }
5444
+ async function readIfExists(p) {
5445
+ try {
5446
+ return await promises.readFile(p, "utf8");
5447
+ } catch {
5448
+ return null;
5449
+ }
5450
+ }
5171
5451
  async function findJayFiles(dir) {
5172
5452
  return await glob(`${dir}/**/*${JAY_EXTENSION$1}`);
5173
5453
  }
@@ -5369,6 +5649,17 @@ function resolveContractTag(contract, tagPath) {
5369
5649
  }
5370
5650
  return void 0;
5371
5651
  }
5652
+ function checkJsonLdScripts(jayHtml) {
5653
+ const errors = [];
5654
+ for (const script of jayHtml.scripts ?? []) {
5655
+ if (script.attributes?.type !== "application/ld+json") continue;
5656
+ const err = jsonLdBodyError(script.inline);
5657
+ if (err) {
5658
+ errors.push(`<script type="application/ld+json">: ${err} See designer/seo-guide.md.`);
5659
+ }
5660
+ }
5661
+ return errors;
5662
+ }
5372
5663
  function checkRefElementTypes(jayHtml, file) {
5373
5664
  const imports = jayHtml.headlessImports;
5374
5665
  const keyMap = /* @__PURE__ */ new Map();
@@ -6296,14 +6587,12 @@ async function validateJayFiles(options = {}) {
6296
6587
  const reuseByTemplate = /* @__PURE__ */ new Map();
6297
6588
  const cataloguedContractFiles = /* @__PURE__ */ new Set();
6298
6589
  const pageJayHtmlFiles = await findJayFiles(scanDir);
6299
- const componentJayHtmlFiles = await findJayFiles(componentsDir).catch(() => []);
6590
+ const componentJayHtmlFiles = options.compileOnly ? [] : await findJayFiles(componentsDir).catch(() => []);
6300
6591
  const jayHtmlFiles = [...pageJayHtmlFiles, ...componentJayHtmlFiles];
6301
6592
  const componentJayHtmlFileSet = new Set(componentJayHtmlFiles);
6302
- const componentContractFiles = await findContractFiles(componentsDir).catch(
6303
- () => []
6304
- );
6593
+ const componentContractFiles = options.compileOnly ? [] : await findContractFiles(componentsDir).catch(() => []);
6305
6594
  for (const cf of componentContractFiles) cataloguedContractFiles.add(cf);
6306
- const contractFiles = [...await findContractFiles(scanDir), ...componentContractFiles];
6595
+ const contractFiles = options.compileOnly ? [] : [...await findContractFiles(scanDir), ...componentContractFiles];
6307
6596
  const contractNameByFile = /* @__PURE__ */ new Map();
6308
6597
  if (options.verbose) {
6309
6598
  getLogger().info(chalk.gray(`Scanning directory: ${scanDir}`));
@@ -6370,6 +6659,35 @@ async function validateJayFiles(options = {}) {
6370
6659
  continue;
6371
6660
  }
6372
6661
  parsedFiles.push({ relativePath, parsed: parsedFile.val });
6662
+ if (options.compileOnly) {
6663
+ const generatedFile2 = generateElementFile(
6664
+ parsedFile.val,
6665
+ RuntimeMode$1.MainTrusted,
6666
+ GenerateTarget.jay
6667
+ );
6668
+ for (const validation of generatedFile2.validations) {
6669
+ errors.push({
6670
+ file: relativePath,
6671
+ message: validation,
6672
+ stage: "generate"
6673
+ });
6674
+ }
6675
+ const serverElementFile2 = generateServerElementFile(parsedFile.val);
6676
+ for (const validation of serverElementFile2.validations) {
6677
+ errors.push({
6678
+ file: relativePath,
6679
+ message: `[SSR] ${validation}`,
6680
+ stage: "generate"
6681
+ });
6682
+ }
6683
+ if (options.verbose) {
6684
+ const failed = generatedFile2.validations.length > 0 || serverElementFile2.validations.length > 0;
6685
+ getLogger().info(
6686
+ failed ? chalk.red(`❌ ${relativePath}`) : chalk.green(`✓ ${relativePath}`)
6687
+ );
6688
+ }
6689
+ continue;
6690
+ }
6373
6691
  if (content.includes("application/jay-params")) {
6374
6692
  errors.push({
6375
6693
  file: relativePath,
@@ -6461,6 +6779,9 @@ async function validateJayFiles(options = {}) {
6461
6779
  suggestion: finding.suggestion
6462
6780
  });
6463
6781
  }
6782
+ for (const message of checkJsonLdScripts(parsedFile.val)) {
6783
+ errors.push({ file: relativePath, message, stage: "generate" });
6784
+ }
6464
6785
  regionsSeen += collectRegionElements(parsedFile.val.body).length;
6465
6786
  templateImportsSeen += parsedFile.val.headlessImports.filter(
6466
6787
  (imp) => imp.template
@@ -6588,6 +6909,24 @@ async function validateJayFiles(options = {}) {
6588
6909
  }
6589
6910
  }
6590
6911
  }
6912
+ if (options.compileOnly) {
6913
+ return {
6914
+ valid: errors.length === 0,
6915
+ jayHtmlFilesScanned: jayHtmlFiles.length,
6916
+ contractFilesScanned: contractFiles.length,
6917
+ errors,
6918
+ warnings,
6919
+ coverage,
6920
+ pluginValidators: [],
6921
+ designSystemCoverage,
6922
+ designSystemReuse: {
6923
+ perTemplate: {},
6924
+ catalogued: 0,
6925
+ reusedMoreThanOnce: 0,
6926
+ reusedOfCatalogued: 0
6927
+ }
6928
+ };
6929
+ }
6591
6930
  const robotsTxtPath = path$1.resolve(projectRoot, "public/robots.txt");
6592
6931
  if (!fs$1.existsSync(robotsTxtPath)) {
6593
6932
  warnings.push({
@@ -6603,6 +6942,60 @@ async function validateJayFiles(options = {}) {
6603
6942
  suggestion: "Add to .jay config:\n site:\n baseUrl: https://your-domain.com"
6604
6943
  });
6605
6944
  }
6945
+ if (!config.site?.name) {
6946
+ warnings.push({
6947
+ file: ".jay",
6948
+ message: "site.name not configured — Organization/WebSite JSON-LD and llms.txt will not be emitted, reducing how well search engines and agents understand the site.",
6949
+ suggestion: "Add to .jay config:\n site:\n name: Your Site Name\n logo: /logo.png\n description: What your site is about"
6950
+ });
6951
+ }
6952
+ const allowNoArd = config.validations?.["allow-no-ard"] === true;
6953
+ const ardResult = await checkArdFiles(projectRoot);
6954
+ if (ardResult.absent) {
6955
+ if (!allowNoArd) {
6956
+ warnings.push({
6957
+ file: "public/.well-known/ard.json",
6958
+ message: "public/.well-known/ard.json not found — agents cannot discover this site’s resources/APIs via Agentic Resource Discovery.",
6959
+ suggestion: "Author public/.well-known/ard.json (ARD v0.91), or suppress with `validations: { allow-no-ard: true }` in .jay if the site has no agent resources to advertise. See developer/seo-guide.md."
6960
+ });
6961
+ }
6962
+ } else {
6963
+ for (const finding of ardResult.findings) {
6964
+ if (finding.severity === "error") {
6965
+ errors.push({
6966
+ file: "public/.well-known/ard.json",
6967
+ message: finding.message,
6968
+ stage: "generate",
6969
+ suggestion: finding.suggestion
6970
+ });
6971
+ } else {
6972
+ warnings.push({
6973
+ file: "public/.well-known/ard.json",
6974
+ message: finding.message,
6975
+ suggestion: finding.suggestion
6976
+ });
6977
+ }
6978
+ }
6979
+ }
6980
+ const allowNoErrorPages = config.validations?.["allow-no-error-pages"] === true;
6981
+ if (!allowNoErrorPages) {
6982
+ const scanResult = await scanRoutes(scanDir, {
6983
+ jayHtmlFilename: "page.jay-html",
6984
+ compFilename: "page.ts"
6985
+ });
6986
+ const errorPages = scanResult.errorPages ?? [];
6987
+ const tokens = new Set(errorPages.map((ep) => ep.token));
6988
+ const missing = [];
6989
+ if (!tokens.has("404") && !tokens.has("4xx")) missing.push("404");
6990
+ if (!tokens.has("5xx")) missing.push("5xx");
6991
+ if (missing.length > 0) {
6992
+ warnings.push({
6993
+ file: "src/pages",
6994
+ message: `No branded error page for ${missing.join(" and ")} — these responses fall back to plain text instead of your site's layout.`,
6995
+ suggestion: "Add pages/404/page.jay-html and pages/5xx/page.jay-html importing the `error` headless contract, or suppress with `validations: { allow-no-error-pages: true }` in .jay. See designer/error-pages-guide.md."
6996
+ });
6997
+ }
6998
+ }
6606
6999
  const allowNoTemplate = /* @__PURE__ */ new Set();
6607
7000
  let allowNoDesignSystem = false;
6608
7001
  for (const { parsed } of parsedFiles) {
@@ -6748,6 +7141,7 @@ async function runBuildOutputValidation(args) {
6748
7141
  baseUrl
6749
7142
  })
6750
7143
  );
7144
+ findings.push(...checkBuildJsonLd(loaded.manifest));
6751
7145
  for (const instance of oracle.instances) {
6752
7146
  const slowViewState = await loadSlowViewState(instance.cacheAbsPath);
6753
7147
  findings.push(
@@ -6766,6 +7160,8 @@ async function runBuildOutputValidation(args) {
6766
7160
  slowViewState
6767
7161
  })
6768
7162
  );
7163
+ const carryForward = await loadCarryForward(instance.cacheAbsPath);
7164
+ findings.push(...checkInstanceJsonLd({ instanceUrl: instance.url, carryForward }));
6769
7165
  }
6770
7166
  for (const finding of findings) {
6771
7167
  if (finding.severity === "error") {
@@ -7011,7 +7407,8 @@ async function runValidate(scanPath, options) {
7011
7407
  verbose: options.verbose,
7012
7408
  json: options.json,
7013
7409
  tier2: options.tier2,
7014
- buildDir: options.buildDir
7410
+ buildDir: options.buildDir,
7411
+ compileOnly: options.compileOnly
7015
7412
  });
7016
7413
  printJayValidationResult(result, options);
7017
7414
  if (!result.valid) {
@@ -7117,10 +7514,12 @@ async function resolveProductionContext(projectPath, versionOverride) {
7117
7514
  const jayConfigPath = path__default.join(resolvedPath, ".jay");
7118
7515
  let pagesBase = "./src/pages";
7119
7516
  let siteBaseUrl;
7517
+ let site;
7120
7518
  try {
7121
7519
  const jayConfig = YAML.parse(await fs$2.readFile(jayConfigPath, "utf-8"));
7122
7520
  pagesBase = jayConfig?.devServer?.pagesBase || pagesBase;
7123
7521
  siteBaseUrl = jayConfig?.site?.baseUrl;
7522
+ site = jayConfig?.site;
7124
7523
  } catch {
7125
7524
  }
7126
7525
  const version = versionOverride || await resolveVersionFromPackageJson(resolvedPath);
@@ -7130,7 +7529,8 @@ async function resolveProductionContext(projectPath, versionOverride) {
7130
7529
  buildRoot: path__default.join(resolvedPath, "build"),
7131
7530
  version,
7132
7531
  tsConfigFilePath: path__default.join(resolvedPath, "tsconfig.json"),
7133
- siteBaseUrl
7532
+ siteBaseUrl,
7533
+ site
7134
7534
  };
7135
7535
  }
7136
7536
  async function resolveVersionFromPackageJson(projectRoot) {
@@ -7158,7 +7558,7 @@ async function runBuild(projectPath, options) {
7158
7558
  }
7159
7559
  const ctx = await resolveProductionContext(projectPath, options.version);
7160
7560
  await generateDefinitionFiles(ctx);
7161
- const { buildVersion } = await import("./index-BQbkAEhi.js");
7561
+ const { buildVersion } = await import("./index-B5oavmfA.js");
7162
7562
  await buildVersion({
7163
7563
  version: ctx.version,
7164
7564
  projectRoot: ctx.resolvedPath,
@@ -7167,7 +7567,8 @@ async function runBuild(projectPath, options) {
7167
7567
  concurrency: 4,
7168
7568
  tsConfigFilePath: ctx.tsConfigFilePath,
7169
7569
  minify: options.minify,
7170
- siteBaseUrl: ctx.siteBaseUrl
7570
+ siteBaseUrl: ctx.siteBaseUrl,
7571
+ site: ctx.site
7171
7572
  });
7172
7573
  }
7173
7574
  async function generateDefinitionFiles(ctx) {
@@ -7234,7 +7635,8 @@ async function runRebuild(projectPath, options) {
7234
7635
  version: ctx.version,
7235
7636
  target,
7236
7637
  tsConfigFilePath: ctx.tsConfigFilePath,
7237
- siteBaseUrl: ctx.siteBaseUrl
7638
+ siteBaseUrl: ctx.siteBaseUrl,
7639
+ site: ctx.site
7238
7640
  });
7239
7641
  if (result.errors.length > 0) {
7240
7642
  for (const err of result.errors) {
@@ -8286,9 +8688,16 @@ program.command("validate").description("Validate all .jay-html and .jay-contrac
8286
8688
  ).addOption(new Option("--t2", "Alias for --tier-2").hideHelp()).option(
8287
8689
  "--build-dir <dir>",
8288
8690
  "Build backend dir or build root for Tier 2 (default: highest build/v*)"
8691
+ ).option(
8692
+ "--compile-only",
8693
+ "Parse and compile .jay-html only (no project rules — for plugin route source checks)"
8289
8694
  ).action(async (options) => {
8290
8695
  try {
8291
- await runValidate(options.path, { ...options, tier2: options.tier2 || options.t2 });
8696
+ await runValidate(options.path, {
8697
+ ...options,
8698
+ tier2: options.tier2 || options.t2,
8699
+ compileOnly: options.compileOnly
8700
+ });
8292
8701
  } catch (error) {
8293
8702
  if (options.json) {
8294
8703
  getLogger().important(
@@ -8350,6 +8759,7 @@ export {
8350
8759
  listContracts,
8351
8760
  loadConfig,
8352
8761
  materializeContracts,
8762
+ setWellKnownHeaders,
8353
8763
  startDevServer,
8354
8764
  updateConfig
8355
8765
  };