From b9f75b4405dcd75419759e43e7f2150eb8ec627c Mon Sep 17 00:00:00 2001 From: Nima Nejat Date: Mon, 8 Jun 2026 19:42:28 -0700 Subject: [PATCH 1/3] Add WWDC26 App schema compiler support --- README.md | 2 +- examples/wwdc26-apple-intelligence.ts | 45 ++++++++ metrics.json | 6 +- src/cloud/check.ts | 82 +++++++++++++++ src/core/app-enum-generator.ts | 3 + src/core/app-enum-parser.ts | 6 +- src/core/app-enum-validator.ts | 51 +++++++++ src/core/generator.ts | 64 +++++++++++- src/core/index.ts | 4 + src/core/parser.ts | 30 ++++++ src/core/types.ts | 61 +++++++++++ src/core/validator.ts | 144 ++++++++++++++++++++++++++ src/sdk/index.ts | 76 ++++++++++++++ src/templates/index.ts | 94 +++++++++++++++++ tests/cloud/check.test.ts | 57 ++++++++++ tests/core/app-enum-compiler.test.ts | 29 ++++++ tests/core/compiler.test.ts | 77 ++++++++++++++ tests/examples/compile.test.ts | 1 + 18 files changed, 824 insertions(+), 8 deletions(-) create mode 100644 examples/wwdc26-apple-intelligence.ts diff --git a/README.md b/README.md index e6b81c4..dd70f3c 100644 --- a/README.md +++ b/README.md @@ -297,7 +297,7 @@ the CLI fallback, then continue the same workflow check with `--ran-suggest`. ## Public truth -v0.4.29 · 36 MCP tools + 5 prompts · 204 diagnostic codes · 1341 tests · 58 live packages · 26 bundled templates +v0.4.29 · 36 MCP tools + 5 prompts · 214 diagnostic codes · 1346 tests · 58 live packages · 29 bundled templates Public proof is generated from `../public-truth/public-truth.json` via `npm --prefix .. run truth:sync`. diff --git a/examples/wwdc26-apple-intelligence.ts b/examples/wwdc26-apple-intelligence.ts new file mode 100644 index 0000000..cbd218d --- /dev/null +++ b/examples/wwdc26-apple-intelligence.ts @@ -0,0 +1,45 @@ +import { defineEntity, defineIntent, param } from "@axint/compiler"; + +defineEntity({ + name: "TravelMessage", + schemaDomain: "messages", + schema: "AppSchema.MessagesEntity.message", + syncable: true, + indexed: true, + indexedQuery: true, + ownership: "shared", + display: { + title: "name", + subtitle: "thread", + image: "message", + }, + properties: { + id: param.string("Stable message identifier"), + name: param.string("Message summary"), + thread: param.string("Conversation thread"), + }, + query: "string", +}); + +export default defineIntent({ + name: "SummarizeTravelMessage", + title: "Summarize Travel Message", + description: + "Summarizes a shared travel message and prepares it for an app workflow.", + schemaDomain: "messages", + schema: "AppSchema.MessagesIntent.sendMessage", + conformsTo: ["LongRunningIntent", "CancellableIntent"], + supportedModes: "[.foreground, .background]", + allowedExecutionTargets: ".main", + params: { + message: param.entity("TravelMessage", "Message to summarize"), + audience: param.string("Who the summary is for", { required: false }), + }, + perform: async ({ message }) => { + // Swift implementation hint: + // import FoundationModels + // let session = LanguageModelSession() + // Run the prompt against the current OS model, then attach Cloud Check proof. + return { summary: `Replace with Foundation Models output for ${message}` }; + }, +}); diff --git a/metrics.json b/metrics.json index 2dbce52..e9f87b8 100644 --- a/metrics.json +++ b/metrics.json @@ -47,8 +47,8 @@ "axint.create-widget", "axint.create-intent" ], - "bundledTemplates": 26, - "diagnostics": 204, + "bundledTemplates": 29, + "diagnostics": 214, "xcodeFixRules": 33, "xcodeFixRuleCodes": [ "AX701", @@ -86,7 +86,7 @@ "AX748" ], "tests": { - "typescript": 1227, + "typescript": 1232, "python": 114 }, "registryPackages": 58, diff --git a/src/cloud/check.ts b/src/cloud/check.ts index f7961c9..40380aa 100644 --- a/src/cloud/check.ts +++ b/src/cloud/check.ts @@ -1125,6 +1125,7 @@ function inferEvidenceDiagnostics(input: { diagnostics.push( ...diagnosticsFromStateTransitionHangEvidence(evidenceText, source, file) ); + diagnostics.push(...diagnosticsFromWwdc26Readiness(source, evidenceText, file)); if ( input.input.expectedBehavior && @@ -1196,6 +1197,87 @@ function inferEvidenceDiagnostics(input: { return dedupeDiagnostics(diagnostics); } +function diagnosticsFromWwdc26Readiness( + source: string, + evidenceText: string, + file: string +): Diagnostic[] { + const diagnostics: Diagnostic[] = []; + const lower = source.toLowerCase(); + const touchesAppleIntelligence = + /@App(Intent|Entity|Enum)\(schema:/.test(source) || + /\b(AppSchema|SyncableEntity|OwnershipProvidingEntity|IndexedEntityQuery|IntentValueQuery|FoundationModels|LanguageModelSession|SystemLanguageModel|PrivateCloudComputeLanguageModel|@Generable|GenerationSchema|ToolCallingMode)\b/.test( + source + ); + + if (!touchesAppleIntelligence) return diagnostics; + + if ( + /@AppEntity\(schema:/.test(source) && + !/\bSyncableEntity\b/.test(source) && + !/\bTransientAppEntity\b/.test(source) + ) { + diagnostics.push({ + code: "AXCLOUD-WWDC26-SYNCABLE-ENTITY", + severity: "warning", + file, + message: + "This entity adopts an Apple app schema but does not declare SyncableEntity for stable cross-device identifiers.", + suggestion: + "If the entity represents durable user content, adopt SyncableEntity and make sure id values remain stable across devices.", + }); + } + + if ( + /@AppIntent\(schema:/.test(source) && + /\b(delete|remove|archive|send|update|publish|share)\b/.test(lower) && + !/\bOwnershipProvidingEntity\b/.test(source) + ) { + diagnostics.push({ + code: "AXCLOUD-WWDC26-OWNERSHIP-GUARD", + severity: "warning", + file, + message: + "This schema-backed intent appears to perform a sensitive or destructive action without an OwnershipProvidingEntity guard.", + suggestion: + "For shared or public entities, adopt OwnershipProvidingEntity and return EntityOwnership so the system can ask for confirmation when needed.", + }); + } + + if (/\bIndexedEntity\b/.test(source) && !/\bIndexedEntityQuery\b/.test(source)) { + diagnostics.push({ + code: "AXCLOUD-WWDC26-INDEXED-QUERY", + severity: "warning", + file, + message: + "This entity is indexed for Spotlight, but its query does not adopt IndexedEntityQuery for SDK-level reindex support.", + suggestion: + "Adopt IndexedEntityQuery on the entity query when the entity should be retrieved from Spotlight by identifier.", + }); + } + + if ( + /\b(FoundationModels|LanguageModelSession|SystemLanguageModel|PrivateCloudComputeLanguageModel)\b/.test( + source + ) && + !/\b(xcode\s*27|ios\s*27|ipados\s*27|macos\s*27|visionos\s*27|model version|prompt version|tokenCount|contextSize)\b/.test( + evidenceText + ) + ) { + diagnostics.push({ + code: "AXCLOUD-WWDC26-MODEL-PROOF", + severity: "warning", + file, + message: + "Foundation Models code needs prompt/model-version proof against the current OS model before it is safe to call demo-ready.", + suggestion: + "Attach Xcode 27 build proof plus prompt-version notes, token/context checks, or a focused model behavior test.", + }); + } + + return diagnostics; +} + function compactCloudCheckReport(report: CloudCheckReport): Omit< CloudCheckReport, "swiftCode" diff --git a/src/core/app-enum-generator.ts b/src/core/app-enum-generator.ts index 742ea97..91fb18f 100644 --- a/src/core/app-enum-generator.ts +++ b/src/core/app-enum-generator.ts @@ -17,6 +17,9 @@ export function generateSwiftAppEnum(appEnum: IRAppEnum): string { lines.push(``); lines.push(`import AppIntents`); lines.push(``); + if (appEnum.schema) { + lines.push(`@AppEnum(schema: ${appEnum.schema})`); + } lines.push(`enum ${appEnum.name}: String, AppEnum {`); for (const c of appEnum.cases) { diff --git a/src/core/app-enum-parser.ts b/src/core/app-enum-parser.ts index 4e7c859..f52aa00 100644 --- a/src/core/app-enum-parser.ts +++ b/src/core/app-enum-parser.ts @@ -11,7 +11,7 @@ */ import ts from "typescript"; -import type { IRAppEnum, IRAppEnumCase } from "./types.js"; +import type { IRAppEnum, IRAppEnumCase, IRAppSchemaDomain } from "./types.js"; import { ParserError } from "./parser.js"; import { findCallExpression, @@ -68,6 +68,8 @@ export function parseAppEnumSource( } const title = readStringLiteral(props.get("title")) ?? name; + const schema = readStringLiteral(props.get("schema")); + const schemaDomain = readStringLiteral(props.get("schemaDomain")); const cases = parseCases(props.get("cases"), filePath, sourceFile); if (cases.length === 0) { @@ -83,6 +85,8 @@ export function parseAppEnumSource( return { name, title, + schema: schema || undefined, + schemaDomain: (schemaDomain as IRAppSchemaDomain | null) || undefined, cases, sourceFile: filePath, }; diff --git a/src/core/app-enum-validator.ts b/src/core/app-enum-validator.ts index 4669eb6..2cdaa97 100644 --- a/src/core/app-enum-validator.ts +++ b/src/core/app-enum-validator.ts @@ -15,6 +15,32 @@ import type { Diagnostic, IRAppEnum } from "./types.js"; +const APP_SCHEMA_DOMAINS = new Set([ + "assistant", + "audio", + "books", + "browser", + "calendar", + "camera", + "clock", + "files", + "journaling", + "mail", + "maps", + "messages", + "notes", + "phone", + "photos", + "presentation", + "reader", + "reminders", + "spreadsheet", + "system-search", + "visual-intelligence", + "whiteboard", + "word-processor", +]); + export function validateAppEnum(appEnum: IRAppEnum): Diagnostic[] { const diagnostics: Diagnostic[] = []; @@ -38,6 +64,27 @@ export function validateAppEnum(appEnum: IRAppEnum): Diagnostic[] { return diagnostics; } + if (appEnum.schemaDomain && !APP_SCHEMA_DOMAINS.has(appEnum.schemaDomain)) { + diagnostics.push({ + code: "AX797", + severity: "error", + message: `Unknown Apple app schema domain "${appEnum.schemaDomain}"`, + file: appEnum.sourceFile, + suggestion: `Use one of: ${[...APP_SCHEMA_DOMAINS].join(", ")}`, + }); + } + + if (appEnum.schema && !isSafeSwiftExpression(appEnum.schema)) { + diagnostics.push({ + code: "AX798", + severity: "error", + message: `App Enum schema expression "${appEnum.schema}" is not safe to emit`, + file: appEnum.sourceFile, + suggestion: + 'Use a simple Swift schema reference, e.g. ".messages.messageEffect" or "AppSchema.MessagesEnum.messageEffect".', + }); + } + const seen = new Set(); for (const c of appEnum.cases) { if (!isSwiftIdentifier(c.value)) { @@ -99,6 +146,10 @@ function isSwiftIdentifier(name: string): boolean { return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) && !SWIFT_KEYWORDS.has(name); } +function isSafeSwiftExpression(expression: string): boolean { + return /^\.?[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(expression); +} + const SWIFT_KEYWORDS: ReadonlySet = new Set([ "associatedtype", "class", diff --git a/src/core/generator.ts b/src/core/generator.ts index 35f5dd6..4695f31 100644 --- a/src/core/generator.ts +++ b/src/core/generator.ts @@ -72,6 +72,9 @@ export function generateSwift(intent: IRIntent): string { lines.push(...generatedFileHeader(`${intent.name}Intent.swift`)); lines.push(``); lines.push(`import AppIntents`); + if (intentUsesCoreTransferable(safeIntent)) { + lines.push(`import CoreTransferable`); + } lines.push(`import Foundation`); lines.push(``); @@ -91,7 +94,12 @@ export function generateSwift(intent: IRIntent): string { } // Struct declaration - lines.push(`struct ${safeIntent.name}Intent: AppIntent {`); + if (safeIntent.schema) { + lines.push(`@AppIntent(schema: ${safeIntent.schema})`); + } + lines.push( + `struct ${safeIntent.name}Intent: ${intentConformances(safeIntent).join(", ")} {` + ); // Static metadata lines.push( @@ -103,6 +111,16 @@ export function generateSwift(intent: IRIntent): string { if (safeIntent.isDiscoverable !== undefined) { lines.push(` static let isDiscoverable: Bool = ${safeIntent.isDiscoverable}`); } + if (safeIntent.supportedModes) { + lines.push( + ` static var supportedModes: IntentModes { ${safeIntent.supportedModes} }` + ); + } + if (safeIntent.allowedExecutionTargets) { + lines.push( + ` static var allowedExecutionTargets: ExecutionTargets { ${safeIntent.allowedExecutionTargets} }` + ); + } lines.push(``); // Parameters @@ -160,7 +178,10 @@ export function generateEntity(entity: IREntity): string { const lines: string[] = []; const propertyNames = new Set(entity.properties.map((p) => p.name)); - lines.push(`struct ${entity.name}: AppEntity {`); + if (entity.schema) { + lines.push(`@AppEntity(schema: ${entity.schema})`); + } + lines.push(`struct ${entity.name}: ${entityConformances(entity).join(", ")} {`); lines.push(` static var defaultQuery = ${entity.name}Query()`); lines.push(``); @@ -214,6 +235,18 @@ export function generateEntity(entity: IREntity): string { lines.push(` )`); lines.push(` }`); + if (entity.ownership) { + lines.push(``); + lines.push(` var ownership: EntityOwnership { .${entity.ownership} }`); + } + + if (entity.intentValueRepresentation) { + lines.push(``); + lines.push(` static var transferRepresentation: some TransferRepresentation {`); + lines.push(` ${entity.intentValueRepresentation}`); + lines.push(` }`); + } + lines.push(`}`); return lines.join("\n"); @@ -234,7 +267,10 @@ export function generateEntityQuery(entity: IREntity): string { : queryType === "property" ? "EntityPropertyQuery" : "EntityQuery"; - lines.push(`struct ${entity.name}Query: ${protocol} {`); + const queryConformances = entity.indexedQuery + ? [protocol, "IndexedEntityQuery"] + : [protocol]; + lines.push(`struct ${entity.name}Query: ${queryConformances.join(", ")} {`); if (queryType === "all" || queryType === "property") { lines.push( ` static var findIntentDescription: IntentDescription = IntentDescription("Find ${escapeSwiftString(entity.name)}")` @@ -409,8 +445,30 @@ const APP_INTENT_MEMBER_NAMES = new Set([ "isDiscoverable", "openAppWhenRun", "authenticationPolicy", + "supportedModes", + "allowedExecutionTargets", ]); +function intentUsesCoreTransferable(intent: IRIntent): boolean { + return (intent.entities ?? []).some((entity) => !!entity.intentValueRepresentation); +} + +function intentConformances(intent: IRIntent): string[] { + const conformances = ["AppIntent"]; + for (const conformance of intent.conformsTo ?? []) { + if (!conformances.includes(conformance)) conformances.push(conformance); + } + return conformances; +} + +function entityConformances(entity: IREntity): string[] { + const conformances = ["AppEntity"]; + if (entity.syncable) conformances.push("SyncableEntity"); + if (entity.indexed) conformances.push("IndexedEntity"); + if (entity.ownership) conformances.push("OwnershipProvidingEntity"); + return conformances; +} + function makeIntentParametersSwiftSafe(intent: IRIntent): IRIntent { const renames = new Map(); const used = new Set(); diff --git a/src/core/index.ts b/src/core/index.ts index 94b538e..1836051 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -89,9 +89,13 @@ export { scene, storage, activityState, + appSchemaDomains, } from "../sdk/index.js"; export type { IntentDefinition, + AppSchemaDomain, + AppIntentConformance, + EntityOwnership, EntityDefinition, EntityDisplay, ViewDefinition, diff --git a/src/core/parser.ts b/src/core/parser.ts index 97e45eb..4fee903 100644 --- a/src/core/parser.ts +++ b/src/core/parser.ts @@ -21,6 +21,9 @@ import type { IREntity, DisplayRepresentation, IRParameterSummary, + IRAppSchemaDomain, + IRIntentConformance, + IREntityOwnership, } from "./types.js"; import { PARAM_TYPES, LEGACY_PARAM_ALIASES, isPrimitiveType } from "./types.js"; import { @@ -96,6 +99,8 @@ export function parseIntentSource( const title = readStringLiteral(props.get("title")); const description = readStringLiteral(props.get("description")); const domain = readStringLiteral(props.get("domain")); + const schemaDomain = readStringLiteral(props.get("schemaDomain")); + const schema = readStringLiteral(props.get("schema")); const category = readStringLiteral(props.get("category")); const isDiscoverable = readBooleanLiteral(props.get("isDiscoverable")); const parameterSummary = props.get("parameterSummary") @@ -164,11 +169,17 @@ export function parseIntentSource( const customResultTypeNode = props.get("customResultType"); const customResultType = readStringLiteral(customResultTypeNode); + const conformsTo = readStringArray(props.get("conformsTo")); + const supportedModes = readStringLiteral(props.get("supportedModes")); + const allowedExecutionTargets = readStringLiteral(props.get("allowedExecutionTargets")); + return { name, title, description, domain: domain || undefined, + schemaDomain: (schemaDomain as IRAppSchemaDomain | null) || undefined, + schema: schema || undefined, category: category || undefined, parameters, returnType, @@ -180,6 +191,9 @@ export function parseIntentSource( entities: entities.length > 0 ? entities : undefined, donateOnPerform: donateOnPerform ?? undefined, customResultType: customResultType ?? undefined, + conformsTo: conformsTo.length > 0 ? (conformsTo as IRIntentConformance[]) : undefined, + supportedModes: supportedModes || undefined, + allowedExecutionTargets: allowedExecutionTargets || undefined, }; } @@ -280,12 +294,28 @@ function parseEntityDefinition( const queryTypeNode = props.get("query"); const queryTypeStr = readStringLiteral(queryTypeNode); const queryType = validateQueryType(queryTypeStr, filePath, sourceFile, queryTypeNode); + const schema = readStringLiteral(props.get("schema")); + const schemaDomain = readStringLiteral(props.get("schemaDomain")); + const syncable = readBooleanLiteral(props.get("syncable")); + const indexed = readBooleanLiteral(props.get("indexed")); + const indexedQuery = readBooleanLiteral(props.get("indexedQuery")); + const ownership = readStringLiteral(props.get("ownership")); + const intentValueRepresentation = readStringLiteral( + props.get("intentValueRepresentation") + ); return { name, displayRepresentation, properties, queryType, + schema: schema || undefined, + schemaDomain: (schemaDomain as IRAppSchemaDomain | null) || undefined, + syncable: syncable ?? undefined, + indexed: indexed ?? undefined, + indexedQuery: indexedQuery ?? undefined, + ownership: (ownership as IREntityOwnership | null) || undefined, + intentValueRepresentation: intentValueRepresentation || undefined, }; } diff --git a/src/core/types.ts b/src/core/types.ts index 6934f07..e635ee9 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -52,6 +52,39 @@ export interface DisplayRepresentation { image?: string; } +export type IRAppSchemaDomain = + | "assistant" + | "audio" + | "books" + | "browser" + | "calendar" + | "camera" + | "clock" + | "files" + | "journaling" + | "mail" + | "maps" + | "messages" + | "notes" + | "phone" + | "photos" + | "presentation" + | "reader" + | "reminders" + | "spreadsheet" + | "system-search" + | "visual-intelligence" + | "whiteboard" + | "word-processor"; + +export type IRIntentConformance = + | "LongRunningIntent" + | "CancellableIntent" + | "UndoableIntent" + | "RunSystemShortcutIntent"; + +export type IREntityOwnership = "unknown" | "shared" | "public"; + /** * An App Entity definition for complex, domain-specific data types. * Entities can be queried and used as parameter types in intents. @@ -61,6 +94,20 @@ export interface IREntity { displayRepresentation: DisplayRepresentation; properties: IRParameter[]; queryType: "all" | "id" | "string" | "property"; + /** Swift expression for Apple's `@AppEntity(schema:)` macro. */ + schema?: string; + /** Apple app schema domain bucket this entity belongs to. */ + schemaDomain?: IRAppSchemaDomain; + /** Adopt `SyncableEntity` for stable cross-device identifiers. */ + syncable?: boolean; + /** Adopt `IndexedEntity` so Spotlight can include this entity. */ + indexed?: boolean; + /** Adopt `OwnershipProvidingEntity` with the selected ownership scope. */ + ownership?: IREntityOwnership; + /** Swift expression inserted into `transferRepresentation`. */ + intentValueRepresentation?: string; + /** Make the generated query adopt `IndexedEntityQuery`. */ + indexedQuery?: boolean; } export type IRParameterSummary = @@ -151,6 +198,10 @@ export interface IRIntent { title: string; description: string; domain?: string; + /** Apple app schema domain bucket, e.g. `mail`, `messages`, or `calendar`. */ + schemaDomain?: IRAppSchemaDomain; + /** Swift expression for Apple's `@AppIntent(schema:)` macro. */ + schema?: string; category?: string; parameters: IRParameter[]; returnType: IRType; @@ -169,6 +220,12 @@ export interface IRIntent { donateOnPerform?: boolean; /** Custom result type (SwiftUI view or custom struct) to return */ customResultType?: string; + /** Extra App Intents protocols introduced for Apple Intelligence workflows. */ + conformsTo?: IRIntentConformance[]; + /** Swift expression for `supportedModes`, e.g. `.foreground` or `[.foreground, .background]`. */ + supportedModes?: string; + /** Swift expression for `allowedExecutionTargets`, e.g. `.main`. */ + allowedExecutionTargets?: string; } // ─── Widget IR Types ──────────────────────────────────────────────────────── @@ -335,6 +392,10 @@ export interface IRAppEnum { name: string; /** Display title shown in Shortcuts (defaults to name when omitted). */ title: string; + /** Apple app schema domain bucket this enum belongs to. */ + schemaDomain?: IRAppSchemaDomain; + /** Swift expression for Apple's `@AppEnum(schema:)` macro. */ + schema?: string; cases: IRAppEnumCase[]; sourceFile: string; } diff --git a/src/core/validator.ts b/src/core/validator.ts index 86e068e..4ec69c7 100644 --- a/src/core/validator.ts +++ b/src/core/validator.ts @@ -39,6 +39,41 @@ const HEALTHKIT_USAGE_DESCRIPTION_ALIASES = new Map([ const PRIVACY_USAGE_DESCRIPTION_PATTERN = /^NS[A-Za-z0-9]+UsageDescription$/; +const APP_SCHEMA_DOMAINS = new Set([ + "assistant", + "audio", + "books", + "browser", + "calendar", + "camera", + "clock", + "files", + "journaling", + "mail", + "maps", + "messages", + "notes", + "phone", + "photos", + "presentation", + "reader", + "reminders", + "spreadsheet", + "system-search", + "visual-intelligence", + "whiteboard", + "word-processor", +]); + +const APP_INTENT_CONFORMANCES = new Set([ + "LongRunningIntent", + "CancellableIntent", + "UndoableIntent", + "RunSystemShortcutIntent", +]); + +const ENTITY_OWNERSHIP_VALUES = new Set(["unknown", "shared", "public"]); + /** * Validate an IR intent for App Intents framework compliance. */ @@ -140,6 +175,55 @@ export function validateIntent(intent: IRIntent): Diagnostic[] { seen.add(param.name); } + if (intent.schemaDomain && !APP_SCHEMA_DOMAINS.has(intent.schemaDomain)) { + diagnostics.push({ + code: "AX119", + severity: "error", + message: `Unknown Apple app schema domain "${intent.schemaDomain}"`, + file: intent.sourceFile, + suggestion: `Use one of: ${[...APP_SCHEMA_DOMAINS].join(", ")}`, + }); + } + + if (intent.schema && !isSafeSwiftExpression(intent.schema)) { + diagnostics.push({ + code: "AX120", + severity: "error", + message: `Intent schema expression "${intent.schema}" is not safe to emit`, + file: intent.sourceFile, + suggestion: + 'Use a simple Swift schema reference, e.g. ".mail.createDraft" or "AppSchema.MailIntent.createDraft".', + }); + } + + for (const conformance of intent.conformsTo ?? []) { + if (!APP_INTENT_CONFORMANCES.has(conformance)) { + diagnostics.push({ + code: "AX121", + severity: "error", + message: `Unsupported App Intent conformance "${conformance}"`, + file: intent.sourceFile, + suggestion: `Use one of: ${[...APP_INTENT_CONFORMANCES].join(", ")}`, + }); + } + } + + for (const [label, expression] of [ + ["supportedModes", intent.supportedModes], + ["allowedExecutionTargets", intent.allowedExecutionTargets], + ] as const) { + if (expression && !isSafeSwiftOptionExpression(expression)) { + diagnostics.push({ + code: "AX122", + severity: "error", + message: `${label} expression "${expression}" is not safe to emit`, + file: intent.sourceFile, + suggestion: + 'Use a simple Swift option expression like ".foreground", ".background", ".main", or "[.main, .widgetKitExtension]".', + }); + } + } + // Rule: Entitlement strings must look like reverse-DNS identifiers for (const ent of intent.entitlements ?? []) { const canonicalEntitlement = HEALTHKIT_ENTITLEMENT_ALIASES.get(ent); @@ -302,6 +386,51 @@ export function validateEntity(entity: IREntity, sourceFile: string): Diagnostic }); } + if (entity.schemaDomain && !APP_SCHEMA_DOMAINS.has(entity.schemaDomain)) { + diagnostics.push({ + code: "AX123", + severity: "error", + message: `Unknown Apple app schema domain "${entity.schemaDomain}"`, + file: sourceFile, + suggestion: `Use one of: ${[...APP_SCHEMA_DOMAINS].join(", ")}`, + }); + } + + if (entity.schema && !isSafeSwiftExpression(entity.schema)) { + diagnostics.push({ + code: "AX124", + severity: "error", + message: `Entity schema expression "${entity.schema}" is not safe to emit`, + file: sourceFile, + suggestion: + 'Use a simple Swift schema reference, e.g. ".messages.message" or "AppSchema.MessagesEntity.message".', + }); + } + + if (entity.ownership && !ENTITY_OWNERSHIP_VALUES.has(entity.ownership)) { + diagnostics.push({ + code: "AX125", + severity: "error", + message: `Entity ownership "${entity.ownership}" is not valid`, + file: sourceFile, + suggestion: 'Use "unknown", "shared", or "public".', + }); + } + + if ( + entity.intentValueRepresentation && + !isSafeIntentValueRepresentation(entity.intentValueRepresentation) + ) { + diagnostics.push({ + code: "AX126", + severity: "error", + message: "intentValueRepresentation must be a single safe Swift expression", + file: sourceFile, + suggestion: + "Use an IntentValueRepresentation(...) expression and avoid semicolons, comments, or braces.", + }); + } + return diagnostics; } @@ -365,3 +494,18 @@ function isPlaceholderUsageDescription(value: string): boolean { normalized.includes("insert usage description") ); } + +function isSafeSwiftExpression(expression: string): boolean { + return /^\.?[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(expression); +} + +function isSafeSwiftOptionExpression(expression: string): boolean { + return /^[A-Za-z0-9_.,\s[\]()]+$/.test(expression) && !/[{};:]/.test(expression); +} + +function isSafeIntentValueRepresentation(expression: string): boolean { + return ( + /^IntentValueRepresentation\s*\([^{};]*\)$/.test(expression.trim()) && + !/\/\/|\/\*/.test(expression) + ); +} diff --git a/src/sdk/index.ts b/src/sdk/index.ts index a2c9bae..8670e22 100644 --- a/src/sdk/index.ts +++ b/src/sdk/index.ts @@ -36,6 +36,42 @@ // ─── Shared Config ─────────────────────────────────────────────────── +export const appSchemaDomains = { + assistant: "assistant", + audio: "audio", + books: "books", + browser: "browser", + calendar: "calendar", + camera: "camera", + clock: "clock", + files: "files", + journaling: "journaling", + mail: "mail", + maps: "maps", + messages: "messages", + notes: "notes", + phone: "phone", + photos: "photos", + presentation: "presentation", + reader: "reader", + reminders: "reminders", + spreadsheet: "spreadsheet", + systemSearch: "system-search", + visualIntelligence: "visual-intelligence", + whiteboard: "whiteboard", + wordProcessor: "word-processor", +} as const; + +export type AppSchemaDomain = (typeof appSchemaDomains)[keyof typeof appSchemaDomains]; + +export type AppIntentConformance = + | "LongRunningIntent" + | "CancellableIntent" + | "UndoableIntent" + | "RunSystemShortcutIntent"; + +export type EntityOwnership = "unknown" | "shared" | "public"; + /** Configuration for a single parameter. */ export interface ParamConfig { /** Display name for this parameter (auto-generated from field name if omitted). */ @@ -170,6 +206,16 @@ export interface IntentDefinition< * "social", "commerce", "media", "navigation", "smart-home" */ domain?: string; + /** + * Apple app schema domain bucket from WWDC26, such as "mail", + * "messages", "calendar", "reminders", or "visual-intelligence". + */ + schemaDomain?: AppSchemaDomain; + /** + * Swift expression emitted in `@AppIntent(schema: ...)`. + * Example: `".mail.createDraft"` or `"AppSchema.MailIntent.createDraft"`. + */ + schema?: string; /** Siri/Shortcuts category for discoverability. */ category?: string; /** @@ -203,6 +249,12 @@ export interface IntentDefinition< perform: (params: { [K in keyof TParams]: unknown; }) => Promise; + /** Additional App Intents protocols for WWDC26 workflows. */ + conformsTo?: AppIntentConformance[]; + /** Swift expression emitted as `static var supportedModes`. */ + supportedModes?: string; + /** Swift expression emitted as `static var allowedExecutionTargets`. */ + allowedExecutionTargets?: string; } /** @@ -449,6 +501,13 @@ export interface EntityDisplay { export interface EntityDefinition { /** PascalCase name for the generated Swift struct. */ name: string; + /** + * Swift expression emitted in `@AppEntity(schema: ...)`. + * Example: `".messages.message"` or `"AppSchema.MessagesEntity.message"`. + */ + schema?: string; + /** Apple app schema domain bucket this entity belongs to. */ + schemaDomain?: AppSchemaDomain; /** How the entity is displayed in Siri/Shortcuts. */ display: EntityDisplay; /** Entity properties using `param.*` helpers. */ @@ -461,6 +520,16 @@ export interface EntityDefinition { * - "property" for EntityPropertyQuery */ query?: "all" | "id" | "string" | "property"; + /** Adopt `SyncableEntity` for stable cross-device identifiers. */ + syncable?: boolean; + /** Adopt `IndexedEntity` so Spotlight can include this entity. */ + indexed?: boolean; + /** Make the generated query adopt `IndexedEntityQuery`. */ + indexedQuery?: boolean; + /** Adopt `OwnershipProvidingEntity` and emit `var ownership`. */ + ownership?: EntityOwnership; + /** Swift expression inserted into `transferRepresentation`. */ + intentValueRepresentation?: string; } /** @@ -799,6 +868,13 @@ export interface AppEnumCaseConfig { export interface AppEnumDefinition { /** PascalCase type name — becomes the Swift enum identifier. */ name: string; + /** + * Swift expression emitted in `@AppEnum(schema: ...)`. + * Example: `".messages.messageEffect"` or `"AppSchema.MessagesEnum.messageEffect"`. + */ + schema?: string; + /** Apple app schema domain bucket this enum belongs to. */ + schemaDomain?: AppSchemaDomain; /** Type display representation shown in Shortcuts (falls back to name). */ title?: string; /** The ordered list of cases. */ diff --git a/src/templates/index.ts b/src/templates/index.ts index 7f6c158..ea2d282 100644 --- a/src/templates/index.ts +++ b/src/templates/index.ts @@ -829,6 +829,97 @@ export default defineIntent({ `, }; +const foundationModelSession: IntentTemplate = { + id: "foundation-model-session", + name: "foundation-model-session", + title: "Foundation Model Session", + domain: "apple-intelligence", + category: "foundation-models", + description: + "Scaffold an App Intent that hands work to Apple's Foundation Models framework.", + source: `import { defineIntent, param } from "@axint/compiler"; + +export default defineIntent({ + name: "SummarizeWithModel", + title: "Summarize With Model", + description: "Summarizes text with an Apple Foundation Models session.", + schemaDomain: "assistant", + params: { + sourceText: param.string("Text to summarize"), + audience: param.string("Who the summary is for", { required: false }), + }, + perform: async ({ sourceText }) => { + // Swift implementation hint: + // import FoundationModels + // let session = LanguageModelSession() + // let response = try await session.respond(to: Prompt("Summarize: ...")) + return { summary: "Replace with Foundation Models output" }; + }, +}); +`, +}; + +const foundationModelTool: IntentTemplate = { + id: "foundation-model-tool", + name: "foundation-model-tool", + title: "Foundation Model Tool", + domain: "apple-intelligence", + category: "foundation-models", + description: + "Scaffold a model-backed App Intent with a place to wire a FoundationModels Tool.", + source: `import { defineIntent, param } from "@axint/compiler"; + +export default defineIntent({ + name: "PlanWithTool", + title: "Plan With Tool", + description: "Uses a model tool to produce a structured app-side plan.", + schemaDomain: "assistant", + params: { + request: param.string("User request"), + context: param.string("Relevant app context", { required: false }), + }, + perform: async ({ request }) => { + // Swift implementation hint: + // import FoundationModels + // Define a Tool that reads app data or performs safe side effects, + // then pass it into LanguageModelSession for tool-calling. + return { plan: "Replace with Foundation Models tool output" }; + }, +}); +`, +}; + +const privateCloudModelIntent: IntentTemplate = { + id: "private-cloud-model-intent", + name: "private-cloud-model-intent", + title: "Private Cloud Model Intent", + domain: "apple-intelligence", + category: "foundation-models", + description: + "Scaffold an intent that can move from on-device models to Private Cloud Compute.", + source: `import { defineIntent, param } from "@axint/compiler"; + +export default defineIntent({ + name: "ReasonWithPrivateCloud", + title: "Reason With Private Cloud", + description: "Runs a higher-context model workflow for app-specific reasoning.", + schemaDomain: "assistant", + supportedModes: "[.foreground, .background]", + params: { + task: param.string("Reasoning task"), + constraints: param.string("Constraints to respect", { required: false }), + }, + perform: async ({ task }) => { + // Swift implementation hint: + // import FoundationModels + // Use SystemLanguageModel first, then adopt PrivateCloudComputeLanguageModel + // when the task needs larger context or deeper reasoning. + return { result: "Replace with Private Cloud Compute model output" }; + }, +}); +`, +}; + // ─── Registry ──────────────────────────────────────────────────────── export const TEMPLATES: IntentTemplate[] = [ @@ -858,6 +949,9 @@ export const TEMPLATES: IntentTemplate[] = [ addToCart, bookAppointment, runShortcut, + foundationModelSession, + foundationModelTool, + privateCloudModelIntent, ]; /** @deprecated Use TEMPLATES. Kept for v0.1.x import compatibility. */ diff --git a/tests/cloud/check.test.ts b/tests/cloud/check.test.ts index 0dd5be6..2d07161 100644 --- a/tests/cloud/check.test.ts +++ b/tests/cloud/check.test.ts @@ -87,6 +87,63 @@ export default defineIntent({ expect(report.swiftCode).toContain("struct SendMessageIntent"); }); + it("flags WWDC26 schema-backed entities that miss continuity and ownership proof", () => { + const report = runCloudCheck({ + fileName: "SendMessageIntent.swift", + source: ` +import AppIntents + +@AppEntity(schema: AppSchema.MessagesEntity.message) +struct Message: AppEntity, IndexedEntity { + static var defaultQuery = MessageQuery() + var id: String + var name: String + static let typeDisplayRepresentation: TypeDisplayRepresentation = "Message" + var displayRepresentation: DisplayRepresentation { "\\(name)" } +} + +struct MessageQuery: EntityQuery { + func entities(for identifiers: [Message.ID]) async throws -> [Message] { [] } +} + +@AppIntent(schema: AppSchema.MessagesIntent.sendMessage) +struct SendMessageIntent: AppIntent { + static var title: LocalizedStringResource = "Send Message" + static var description: IntentDescription = "Send a message" + func perform() async throws -> some IntentResult { .result() } +} +`, + }); + + const codes = report.diagnostics.map((d) => d.code); + expect(codes).toContain("AXCLOUD-WWDC26-SYNCABLE-ENTITY"); + expect(codes).toContain("AXCLOUD-WWDC26-OWNERSHIP-GUARD"); + expect(codes).toContain("AXCLOUD-WWDC26-INDEXED-QUERY"); + expect(report.status).toBe("needs_review"); + }); + + it("asks Foundation Models code for model-version proof", () => { + const report = runCloudCheck({ + fileName: "ModelSummary.swift", + source: ` +import AppIntents +import FoundationModels + +struct SummarizeIntent: AppIntent { + static var title: LocalizedStringResource = "Summarize" + func perform() async throws -> some IntentResult { + let session = LanguageModelSession() + _ = session + return .result() + } +} +`, + }); + + expect(report.diagnostics.map((d) => d.code)).toContain("AXCLOUD-WWDC26-MODEL-PROOF"); + expect(report.status).toBe("needs_review"); + }); + it("separates compiler, MCP, cloud ruleset, and expected project versions", () => { const report = runCloudCheck({ fileName: "ContentView.swift", diff --git a/tests/core/app-enum-compiler.test.ts b/tests/core/app-enum-compiler.test.ts index 766d9a2..b887453 100644 --- a/tests/core/app-enum-compiler.test.ts +++ b/tests/core/app-enum-compiler.test.ts @@ -57,6 +57,17 @@ describe("generateSwiftAppEnum", () => { ); }); + it("emits the Apple app schema macro when configured", () => { + const swift = generateSwiftAppEnum({ + ...PIZZA_SIZE, + schemaDomain: "messages", + schema: "AppSchema.MessagesEnum.messageEffect", + }); + + expect(swift).toContain("@AppEnum(schema: AppSchema.MessagesEnum.messageEffect)"); + expect(swift).toContain("enum PizzaSize: String, AppEnum"); + }); + it("uses a plain string DisplayRepresentation when a case has no image", () => { const plain: IRAppEnum = { ...PIZZA_SIZE, @@ -164,6 +175,24 @@ describe("parseAppEnumSource", () => { expect(ir.sourceFile).toBe("pizza.ts"); }); + it("parses schema metadata from defineAppEnum", () => { + const ir = parseAppEnumSource( + ` + import { defineAppEnum } from "@axint/compiler"; + export default defineAppEnum({ + name: "MessageEffect", + schemaDomain: "messages", + schema: "AppSchema.MessagesEnum.messageEffect", + cases: [{ value: "gentle", title: "Gentle" }], + }); + `, + "message-effect.ts" + ); + + expect(ir.schemaDomain).toBe("messages"); + expect(ir.schema).toBe("AppSchema.MessagesEnum.messageEffect"); + }); + it("defaults title to name when omitted", () => { const ir = parseAppEnumSource( ` diff --git a/tests/core/compiler.test.ts b/tests/core/compiler.test.ts index 7de996e..36667b9 100644 --- a/tests/core/compiler.test.ts +++ b/tests/core/compiler.test.ts @@ -123,4 +123,81 @@ export default defineIntent({ "static var parameterSummary: some ParameterSummary" ); }); + + it("emits WWDC26 App schema macros and entity AI affordances", () => { + const source = ` +import { defineIntent, defineEntity, param } from "@axint/sdk"; + +defineEntity({ + name: "Message", + schemaDomain: "messages", + schema: "AppSchema.MessagesEntity.message", + syncable: true, + indexed: true, + indexedQuery: true, + ownership: "shared", + intentValueRepresentation: "IntentValueRepresentation(exporting: \\\\.name)", + display: { + title: "name", + subtitle: "thread", + }, + properties: { + id: param.string("Stable message ID"), + name: param.string("Message summary"), + thread: param.string("Thread title"), + }, + query: "string", +}); + +export default defineIntent({ + name: "SendMessage", + title: "Send Message", + description: "Sends a message through the app", + schemaDomain: "messages", + schema: "AppSchema.MessagesIntent.sendMessage", + conformsTo: ["LongRunningIntent", "CancellableIntent"], + supportedModes: "[.foreground, .background]", + allowedExecutionTargets: ".main", + params: { + target: param.entity("Message", "Message thread"), + body: param.string("Message body"), + }, + perform: async () => { + return { ok: true }; + }, +}); +`; + const result = compileSource(source, "wwdc26.ts"); + + expect(result.success).toBe(true); + expect(result.output?.ir.schemaDomain).toBe("messages"); + expect(result.output?.swiftCode).toContain("import CoreTransferable"); + expect(result.output?.swiftCode).toContain( + "@AppEntity(schema: AppSchema.MessagesEntity.message)" + ); + expect(result.output?.swiftCode).toContain( + "struct Message: AppEntity, SyncableEntity, IndexedEntity, OwnershipProvidingEntity" + ); + expect(result.output?.swiftCode).toContain( + "struct MessageQuery: EntityStringQuery, IndexedEntityQuery" + ); + expect(result.output?.swiftCode).toContain( + "var ownership: EntityOwnership { .shared }" + ); + expect(result.output?.swiftCode).toContain( + "static var transferRepresentation: some TransferRepresentation" + ); + expect(result.output?.swiftCode).toContain( + "@AppIntent(schema: AppSchema.MessagesIntent.sendMessage)" + ); + expect(result.output?.swiftCode).toContain( + "struct SendMessageIntent: AppIntent, LongRunningIntent, CancellableIntent" + ); + expect(result.output?.swiftCode).toContain( + "static var supportedModes: IntentModes { [.foreground, .background] }" + ); + expect(result.output?.swiftCode).toContain( + "static var allowedExecutionTargets: ExecutionTargets { .main }" + ); + }); }); diff --git a/tests/examples/compile.test.ts b/tests/examples/compile.test.ts index c621432..8d5d4b1 100644 --- a/tests/examples/compile.test.ts +++ b/tests/examples/compile.test.ts @@ -34,6 +34,7 @@ const EXPECTED_SURFACES = new Map< ["step-counter.ts", "widget"], ["trail-planner.ts", "intent"], ["weather-app.ts", "app"], + ["wwdc26-apple-intelligence.ts", "intent"], ]); describe("bundled examples", () => { From 8981b6303c73b7d08e1a6d2ad0e340a8ad0f2de9 Mon Sep 17 00:00:00 2001 From: Nima Nejat Date: Mon, 8 Jun 2026 19:53:13 -0700 Subject: [PATCH 2/3] Add WWDC26 P1 intent support --- README.md | 2 +- .../wwdc26-p1-collections-and-progress.ts | 42 +++++ metrics.json | 4 +- src/cloud/check.ts | 72 +++++++- src/core/parser.ts | 82 ++++++++- src/core/types.ts | 13 +- src/core/validator.ts | 11 ++ src/sdk/index.ts | 44 ++++- src/templates/index.ts | 171 ++++++++++++++++++ tests/cloud/check.test.ts | 61 +++++++ tests/core/compiler.test.ts | 48 +++++ tests/examples/compile.test.ts | 1 + 12 files changed, 542 insertions(+), 9 deletions(-) create mode 100644 examples/wwdc26-p1-collections-and-progress.ts diff --git a/README.md b/README.md index dd70f3c..ff045cc 100644 --- a/README.md +++ b/README.md @@ -297,7 +297,7 @@ the CLI fallback, then continue the same workflow check with `--ran-suggest`. ## Public truth -v0.4.29 · 36 MCP tools + 5 prompts · 214 diagnostic codes · 1346 tests · 58 live packages · 29 bundled templates +v0.4.29 · 36 MCP tools + 5 prompts · 214 diagnostic codes · 1350 tests · 58 live packages · 34 bundled templates Public proof is generated from `../public-truth/public-truth.json` via `npm --prefix .. run truth:sync`. diff --git a/examples/wwdc26-p1-collections-and-progress.ts b/examples/wwdc26-p1-collections-and-progress.ts new file mode 100644 index 0000000..67f6652 --- /dev/null +++ b/examples/wwdc26-p1-collections-and-progress.ts @@ -0,0 +1,42 @@ +import { defineEntity, defineIntent, param } from "@axint/compiler"; + +defineEntity({ + name: "ResearchSource", + schemaDomain: "assistant", + schema: "AppSchema.AssistantEntity.document", + syncable: true, + indexed: true, + indexedQuery: true, + ownership: "shared", + display: { + title: "title", + subtitle: "origin", + }, + properties: { + id: param.string("Stable source identifier"), + title: param.string("Source title"), + origin: param.string("Where this source came from"), + }, + query: "string", +}); + +export default defineIntent({ + name: "BuildResearchBrief", + title: "Build Research Brief", + description: + "Builds a research brief from a selected collection of assistant sources.", + schemaDomain: "assistant", + schema: "AppSchema.AssistantIntent.summarize", + conformsTo: ["LongRunningIntent", "ProgressReportingIntent"], + supportedModes: "[.foreground, .background]", + params: { + sources: param.entityCollection("ResearchSource", "Sources to include"), + tags: param.array(param.string("Tag"), "Tags", { required: false }), + }, + perform: async ({ sources }) => { + // Swift implementation hint: + // Use performBackgroundTask(options: LongRunningTaskOptions(...)) and + // report progress as each source is processed. + return { sourceCount: Array.isArray(sources) ? sources.length : 0 }; + }, +}); diff --git a/metrics.json b/metrics.json index e9f87b8..e2fad94 100644 --- a/metrics.json +++ b/metrics.json @@ -47,7 +47,7 @@ "axint.create-widget", "axint.create-intent" ], - "bundledTemplates": 29, + "bundledTemplates": 34, "diagnostics": 214, "xcodeFixRules": 33, "xcodeFixRuleCodes": [ @@ -86,7 +86,7 @@ "AX748" ], "tests": { - "typescript": 1232, + "typescript": 1236, "python": 114 }, "registryPackages": 58, diff --git a/src/cloud/check.ts b/src/cloud/check.ts index 40380aa..984e762 100644 --- a/src/cloud/check.ts +++ b/src/cloud/check.ts @@ -1206,7 +1206,11 @@ function diagnosticsFromWwdc26Readiness( const lower = source.toLowerCase(); const touchesAppleIntelligence = /@App(Intent|Entity|Enum)\(schema:/.test(source) || - /\b(AppSchema|SyncableEntity|OwnershipProvidingEntity|IndexedEntityQuery|IntentValueQuery|FoundationModels|LanguageModelSession|SystemLanguageModel|PrivateCloudComputeLanguageModel|@Generable|GenerationSchema|ToolCallingMode)\b/.test( + /\b(AppSchema|SyncableEntity|OwnershipProvidingEntity|IndexedEntityQuery|IntentValueQuery|FoundationModels|LanguageModelSession|SystemLanguageModel|PrivateCloudComputeLanguageModel|GenerationSchema|ToolCallingMode)\b/.test( + source + ) || + /@(?:Generable|UnionValue)\b/.test(source) || + /\b(LongRunningIntent|ProgressReportingIntent|SnippetIntent|ShowsSnippetIntent|ShowsSnippetView|ResultsCollection|IntentItemCollection|AppUnionValue|AppUnionValueCasesProviding)\b/.test( source ); @@ -1275,6 +1279,72 @@ function diagnosticsFromWwdc26Readiness( }); } + if ( + /\bLongRunningIntent\b/.test(source) && + !/\b(performBackgroundTask|LongRunningTaskOptions)\b/.test( + source + "\n" + evidenceText + ) + ) { + diagnostics.push({ + code: "AXCLOUD-WWDC26-LONG-RUNNING-PROOF", + severity: "warning", + file, + message: + "LongRunningIntent code needs proof that work is wrapped in the SDK long-running background task API.", + suggestion: + "Use performBackgroundTask(options:operation:) with LongRunningTaskOptions, then attach clean Xcode build/run proof for the target OS.", + }); + } + + if ( + /\bProgressReportingIntent\b/.test(source) && + !/(?:\bcompletedUnitCount\b|\btotalUnitCount\b|\bProgress\s*\(|\bprogress\.)/.test( + source + "\n" + evidenceText + ) + ) { + diagnostics.push({ + code: "AXCLOUD-WWDC26-PROGRESS-PROOF", + severity: "warning", + file, + message: + "ProgressReportingIntent code needs observable progress proof before it is demo-ready.", + suggestion: + "Report progress milestones during the long-running operation and attach a focused run log or test showing progress updates.", + }); + } + + if ( + /\b(SnippetIntent|ShowsSnippetIntent|ShowsSnippetView)\b/.test(source) && + !/\b(requestConfirmation|snippetIntent|ShowsSnippetIntent|ShowsSnippetView)\b/.test( + source + "\n" + evidenceText + ) + ) { + diagnostics.push({ + code: "AXCLOUD-WWDC26-SNIPPET-PROOF", + severity: "warning", + file, + message: + "Snippet-backed App Intents need proof that the snippet is actually returned or requested from perform().", + suggestion: + "Return a result that conforms to ShowsSnippetIntent or ShowsSnippetView, or call requestConfirmation(..., snippetIntent: ...), then attach simulator/device proof.", + }); + } + + if ( + /(?:@UnionValue\b|\bAppUnionValue\b)/.test(source) && + !/\bAppUnionValueCasesProviding\b/.test(source) + ) { + diagnostics.push({ + code: "AXCLOUD-WWDC26-UNION-VALUE-CASES", + severity: "warning", + file, + message: + "Union value code needs an AppUnionValueCasesProviding cases type so the system can enumerate supported cases.", + suggestion: + "Define a cases enum that adopts AppUnionValueCasesProviding and attach Xcode proof that the union value resolves in App Intents.", + }); + } + return diagnostics; } diff --git a/src/core/parser.ts b/src/core/parser.ts index 4fee903..a97bdc1 100644 --- a/src/core/parser.ts +++ b/src/core/parser.ts @@ -577,13 +577,21 @@ function extractParamCall( const typeName = expr.expression.name.text; - // For entity and dynamicOptions, the structure differs: + // For entity, entityCollection, array, and dynamicOptions, the structure differs: // - param.entity("EntityName", "description", config?) + // - param.entityCollection("EntityName", "description", config?) + // - param.array(param.string(...), "description", config?) // - param.dynamicOptions("Provider", param.string(...)) let descriptionArg: ts.Expression | undefined; let configArg: ts.Expression | undefined; - if (typeName === "entity" && expr.arguments.length >= 2) { + if ( + (typeName === "entity" || typeName === "entityCollection") && + expr.arguments.length >= 2 + ) { + descriptionArg = expr.arguments[1]; + configArg = expr.arguments[2]; + } else if (typeName === "array" && expr.arguments.length >= 2) { descriptionArg = expr.arguments[1]; configArg = expr.arguments[2]; } else if (typeName === "dynamicOptions" && expr.arguments.length >= 2) { @@ -673,6 +681,74 @@ function resolveParamType( }; } + // Entity collection types: param.entityCollection("EntityName") + if (typeName === "entityCollection") { + if (!callExpr || callExpr.arguments.length === 0) { + throw new ParserError( + "AX024", + "param.entityCollection() requires the entity name as the first argument", + filePath, + posOf(sourceFile, node), + 'Example: param.entityCollection("Task", "Reference multiple entities")' + ); + } + const entityName = readStringLiteral(callExpr.arguments[0]); + if (!entityName) { + throw new ParserError( + "AX025", + "param.entityCollection() requires a string entity name", + filePath, + posOf(sourceFile, node) + ); + } + return { + kind: "array", + elementType: { + kind: "entity", + entityName, + properties: [], + }, + }; + } + + // Array types: param.array(param.string(...), "Description") + if (typeName === "array") { + if (!callExpr || callExpr.arguments.length === 0) { + throw new ParserError( + "AX026", + "param.array() requires an inner param helper as the first argument", + filePath, + posOf(sourceFile, node), + 'Example: param.array(param.string("Tag"), "Tags")' + ); + } + const innerArg = callExpr.arguments[0]; + if ( + ts.isCallExpression(innerArg) && + ts.isPropertyAccessExpression(innerArg.expression) && + ts.isIdentifier(innerArg.expression.expression) && + innerArg.expression.expression.text === "param" + ) { + return { + kind: "array", + elementType: resolveParamType( + innerArg.expression.name.text, + filePath, + sourceFile, + innerArg, + innerArg + ), + }; + } + throw new ParserError( + "AX027", + "param.array() first argument must be a param.* helper", + filePath, + posOf(sourceFile, node), + 'Example: param.array(param.entity("Task", "Task"), "Tasks")' + ); + } + // Dynamic options: param.dynamicOptions("ProviderName", innerType) if (typeName === "dynamicOptions") { if (!callExpr || callExpr.arguments.length < 2) { @@ -726,7 +802,7 @@ function resolveParamType( `Unknown param type: param.${typeName}`, filePath, posOf(sourceFile, node), - `Supported types: ${[...PARAM_TYPES].join(", ")}, entity, dynamicOptions` + `Supported types: ${[...PARAM_TYPES].join(", ")}, entity, entityCollection, array, dynamicOptions` ); } diff --git a/src/core/types.ts b/src/core/types.ts index e635ee9..cf9f946 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -81,7 +81,18 @@ export type IRIntentConformance = | "LongRunningIntent" | "CancellableIntent" | "UndoableIntent" - | "RunSystemShortcutIntent"; + | "RunSystemShortcutIntent" + | "ProgressReportingIntent" + | "SnippetIntent" + | "SystemIntent" + | "ShowInAppSearchResultsIntent" + | "TargetContentProvidingIntent" + | "URLRepresentableIntent" + | "OpenIntent" + | "DeleteIntent" + | "SetValueIntent" + | "ControlConfigurationIntent" + | "WidgetConfigurationIntent"; export type IREntityOwnership = "unknown" | "shared" | "public"; diff --git a/src/core/validator.ts b/src/core/validator.ts index 4ec69c7..45db5a1 100644 --- a/src/core/validator.ts +++ b/src/core/validator.ts @@ -70,6 +70,17 @@ const APP_INTENT_CONFORMANCES = new Set([ "CancellableIntent", "UndoableIntent", "RunSystemShortcutIntent", + "ProgressReportingIntent", + "SnippetIntent", + "SystemIntent", + "ShowInAppSearchResultsIntent", + "TargetContentProvidingIntent", + "URLRepresentableIntent", + "OpenIntent", + "DeleteIntent", + "SetValueIntent", + "ControlConfigurationIntent", + "WidgetConfigurationIntent", ]); const ENTITY_OWNERSHIP_VALUES = new Set(["unknown", "shared", "public"]); diff --git a/src/sdk/index.ts b/src/sdk/index.ts index 8670e22..d959d65 100644 --- a/src/sdk/index.ts +++ b/src/sdk/index.ts @@ -68,7 +68,18 @@ export type AppIntentConformance = | "LongRunningIntent" | "CancellableIntent" | "UndoableIntent" - | "RunSystemShortcutIntent"; + | "RunSystemShortcutIntent" + | "ProgressReportingIntent" + | "SnippetIntent" + | "SystemIntent" + | "ShowInAppSearchResultsIntent" + | "TargetContentProvidingIntent" + | "URLRepresentableIntent" + | "OpenIntent" + | "DeleteIntent" + | "SetValueIntent" + | "ControlConfigurationIntent" + | "WidgetConfigurationIntent"; export type EntityOwnership = "unknown" | "shared" | "public"; @@ -168,6 +179,37 @@ export const param = { ...config, }), + /** Array parameter. Pass another `param.*` helper as the element type. */ + array: ( + innerParam: ReturnType>, + description: string, + config?: Partial + ) => { + const { description: innerDescription, ...inner } = innerParam; + return { + type: "array" as const, + innerType: inner, + description, + innerDescription, + ...config, + }; + }, + + /** + * Entity collection parameter. Compiles to `[EntityName]` for intents + * that operate on a selected group of app entities. + */ + entityCollection: ( + entityName: string, + description: string, + config?: Partial + ) => ({ + type: "entityCollection" as const, + entityName, + description, + ...config, + }), + /** * Parameter with dynamic option suggestions provided at runtime * by a DynamicOptionsProvider. The `providerName` maps to a diff --git a/src/templates/index.ts b/src/templates/index.ts index ea2d282..a01ef3b 100644 --- a/src/templates/index.ts +++ b/src/templates/index.ts @@ -920,6 +920,172 @@ export default defineIntent({ `, }; +const longRunningProgressIntent: IntentTemplate = { + id: "long-running-progress-intent", + name: "long-running-progress-intent", + title: "Long-Running Progress Intent", + domain: "apple-intelligence", + category: "foundation-models", + description: + "Scaffold an intent that marks long-running work and progress reporting explicitly.", + source: `import { defineIntent, param } from "@axint/compiler"; + +export default defineIntent({ + name: "BuildResearchBrief", + title: "Build Research Brief", + description: "Runs a longer model-backed workflow with progress proof.", + schemaDomain: "assistant", + conformsTo: ["LongRunningIntent", "ProgressReportingIntent"], + supportedModes: "[.foreground, .background]", + params: { + topic: param.string("Brief topic"), + sources: param.array(param.string("Source URL or note"), "Sources to include", { + required: false, + }), + }, + perform: async ({ topic }) => { + // Swift implementation hint: + // Use performBackgroundTask(options: LongRunningTaskOptions(...)) for + // background runtime and report progress as milestones complete. + return { briefId: \`brief-\${topic}\` }; + }, +}); +`, +}; + +const interactiveSnippetIntent: IntentTemplate = { + id: "interactive-snippet-intent", + name: "interactive-snippet-intent", + title: "Interactive Snippet Intent", + domain: "apple-intelligence", + category: "snippets", + description: + "Scaffold a snippet-backed App Intent flow for confirmation or follow-up actions.", + source: `import { defineIntent, param } from "@axint/compiler"; + +export default defineIntent({ + name: "ConfirmTicketSearch", + title: "Confirm Ticket Search", + description: "Presents a snippet-backed confirmation before continuing.", + schemaDomain: "assistant", + conformsTo: ["SnippetIntent"], + params: { + eventName: param.string("Event name"), + ticketCount: param.int("Ticket count", { default: 2 }), + }, + perform: async ({ eventName }) => { + // Swift implementation hint: + // Return some IntentResult & ShowsSnippetIntent from the real Swift + // perform() and call requestConfirmation(..., snippetIntent: ...). + return { confirmation: \`Review tickets for \${eventName}\` }; + }, +}); +`, +}; + +const systemShortcutBridge: IntentTemplate = { + id: "system-shortcut-bridge", + name: "system-shortcut-bridge", + title: "System Shortcut Bridge", + domain: "automation", + category: "shortcuts", + description: + "Scaffold an intent that bridges app data into a system shortcut workflow.", + source: `import { defineIntent, param } from "@axint/compiler"; + +export default defineIntent({ + name: "RunMorningAutomation", + title: "Run Morning Automation", + description: "Runs a named system shortcut with app context.", + schemaDomain: "assistant", + conformsTo: ["RunSystemShortcutIntent"], + params: { + shortcutName: param.string("Shortcut name"), + context: param.string("Context payload", { required: false }), + }, + perform: async ({ shortcutName }) => { + // Swift implementation hint: + // Resolve the system shortcut target, pass app context, and attach + // Xcode 27 proof that the target exists on the current OS build. + return { shortcutName }; + }, +}); +`, +}; + +const entityCollectionSearch: IntentTemplate = { + id: "entity-collection-search", + name: "entity-collection-search", + title: "Entity Collection Search", + domain: "apple-intelligence", + category: "entities", + description: "Scaffold an intent that accepts a collection of schema-backed entities.", + source: `import { defineEntity, defineIntent, param } from "@axint/compiler"; + +defineEntity({ + name: "InboxMessage", + schemaDomain: "messages", + schema: "AppSchema.MessagesEntity.message", + syncable: true, + indexed: true, + indexedQuery: true, + ownership: "shared", + display: { + title: "subject", + subtitle: "sender", + }, + properties: { + id: param.string("Stable message identifier"), + subject: param.string("Message subject"), + sender: param.string("Sender name"), + }, + query: "string", +}); + +export default defineIntent({ + name: "SummarizeInboxMessages", + title: "Summarize Inbox Messages", + description: "Summarizes a selected collection of messages.", + schemaDomain: "messages", + schema: "AppSchema.MessagesIntent.sendMessage", + params: { + messages: param.entityCollection("InboxMessage", "Messages to summarize"), + }, + perform: async ({ messages }) => { + return { count: Array.isArray(messages) ? messages.length : 0 }; + }, +}); +`, +}; + +const unionValueRouter: IntentTemplate = { + id: "union-value-router", + name: "union-value-router", + title: "Union Value Router", + domain: "apple-intelligence", + category: "entities", + description: "Scaffold an intent that routes work across union-value style cases.", + source: `import { defineIntent, param } from "@axint/compiler"; + +export default defineIntent({ + name: "RouteAssistantValue", + title: "Route Assistant Value", + description: "Routes an assistant value into the right app workflow.", + schemaDomain: "assistant", + params: { + valueKind: param.string("Union value case"), + payload: param.string("Serialized value payload"), + }, + perform: async ({ valueKind }) => { + // Swift implementation hint: + // Define the concrete Swift union with @UnionValue and make its cases + // enum adopt AppUnionValueCasesProviding before wiring this router. + return { routedTo: valueKind }; + }, +}); +`, +}; + // ─── Registry ──────────────────────────────────────────────────────── export const TEMPLATES: IntentTemplate[] = [ @@ -952,6 +1118,11 @@ export const TEMPLATES: IntentTemplate[] = [ foundationModelSession, foundationModelTool, privateCloudModelIntent, + longRunningProgressIntent, + interactiveSnippetIntent, + systemShortcutBridge, + entityCollectionSearch, + unionValueRouter, ]; /** @deprecated Use TEMPLATES. Kept for v0.1.x import compatibility. */ diff --git a/tests/cloud/check.test.ts b/tests/cloud/check.test.ts index 2d07161..3174c80 100644 --- a/tests/cloud/check.test.ts +++ b/tests/cloud/check.test.ts @@ -144,6 +144,67 @@ struct SummarizeIntent: AppIntent { expect(report.status).toBe("needs_review"); }); + it("asks long-running progress intents for background and progress proof", () => { + const report = runCloudCheck({ + fileName: "ResearchBriefIntent.swift", + source: ` +import AppIntents + +struct ResearchBriefIntent: AppIntent, LongRunningIntent, ProgressReportingIntent { + static var title: LocalizedStringResource = "Research Brief" + func perform() async throws -> some IntentResult { + return .result() + } +} +`, + }); + + const codes = report.diagnostics.map((d) => d.code); + expect(codes).toContain("AXCLOUD-WWDC26-LONG-RUNNING-PROOF"); + expect(codes).toContain("AXCLOUD-WWDC26-PROGRESS-PROOF"); + expect(report.status).toBe("needs_review"); + }); + + it("asks snippet intents for returned snippet proof", () => { + const report = runCloudCheck({ + fileName: "TicketSnippetIntent.swift", + source: ` +import AppIntents + +struct TicketSnippetIntent: AppIntent, SnippetIntent { + static var title: LocalizedStringResource = "Ticket Snippet" + func perform() async throws -> some IntentResult { + return .result() + } +} +`, + }); + + expect(report.diagnostics.map((d) => d.code)).toContain( + "AXCLOUD-WWDC26-SNIPPET-PROOF" + ); + expect(report.status).toBe("needs_review"); + }); + + it("asks union values for cases-provider proof", () => { + const report = runCloudCheck({ + fileName: "AssistantValue.swift", + source: ` +import AppIntents + +@UnionValue +struct AssistantValue: AppUnionValue { + var id: String +} +`, + }); + + expect(report.diagnostics.map((d) => d.code)).toContain( + "AXCLOUD-WWDC26-UNION-VALUE-CASES" + ); + expect(report.status).toBe("needs_review"); + }); + it("separates compiler, MCP, cloud ruleset, and expected project versions", () => { const report = runCloudCheck({ fileName: "ContentView.swift", diff --git a/tests/core/compiler.test.ts b/tests/core/compiler.test.ts index 36667b9..3daab3d 100644 --- a/tests/core/compiler.test.ts +++ b/tests/core/compiler.test.ts @@ -200,4 +200,52 @@ export default defineIntent({ "static var allowedExecutionTargets: ExecutionTargets { .main }" ); }); + + it("emits P1 protocol conformances and entity collection parameters", () => { + const source = ` +import { defineIntent, defineEntity, param } from "@axint/sdk"; + +defineEntity({ + name: "Message", + schemaDomain: "messages", + schema: "AppSchema.MessagesEntity.message", + syncable: true, + indexed: true, + indexedQuery: true, + ownership: "shared", + display: { + title: "name", + }, + properties: { + id: param.string("Stable message ID"), + name: param.string("Message summary"), + }, + query: "string", +}); + +export default defineIntent({ + name: "SummarizeMessages", + title: "Summarize Messages", + description: "Summarizes selected messages with progress", + schemaDomain: "messages", + schema: "AppSchema.MessagesIntent.sendMessage", + conformsTo: ["LongRunningIntent", "ProgressReportingIntent"], + params: { + messages: param.entityCollection("Message", "Messages to summarize"), + tags: param.array(param.string("Tag"), "Tags", { required: false }), + }, + perform: async () => { + return { ok: true }; + }, +}); +`; + const result = compileSource(source, "p1.ts"); + + expect(result.success).toBe(true); + expect(result.output?.swiftCode).toContain( + "struct SummarizeMessagesIntent: AppIntent, LongRunningIntent, ProgressReportingIntent" + ); + expect(result.output?.swiftCode).toContain("var messages: [Message]"); + expect(result.output?.swiftCode).toContain("var tags: [String]?"); + }); }); diff --git a/tests/examples/compile.test.ts b/tests/examples/compile.test.ts index 8d5d4b1..471dea2 100644 --- a/tests/examples/compile.test.ts +++ b/tests/examples/compile.test.ts @@ -35,6 +35,7 @@ const EXPECTED_SURFACES = new Map< ["trail-planner.ts", "intent"], ["weather-app.ts", "app"], ["wwdc26-apple-intelligence.ts", "intent"], + ["wwdc26-p1-collections-and-progress.ts", "intent"], ]); describe("bundled examples", () => { From 32fc21cd46ad82c306d5ac17f23a4bf83f154433 Mon Sep 17 00:00:00 2001 From: Nima Nejat Date: Mon, 8 Jun 2026 20:04:38 -0700 Subject: [PATCH 3/3] Ignore local WWDC SDK snapshots --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f1aa6b7..e959a50 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,7 @@ tests/.tmp-compiler-tests/ # local scratch tmp/ .axint/ +.wwdc/ # Python __pycache__/