From 44b9ee5fde1b417c50e35710e1a468645b34f473 Mon Sep 17 00:00:00 2001 From: McCal-Codes <175259256+McCal-Codes@users.noreply.github.com> Date: Tue, 22 Sep 2026 11:05:00 -0400 Subject: [PATCH] A package can say AI helped make it, and which tools Adds aiAssisted to manifest schema v1: { tools: 1-5 names, note?: text }. The parser reads it with the same limits, so the conformance fuzz agrees. Folio 0.6.6 skips the field rather than refusing the package. Until the Market shows it, AI-6 also keeps the description prefix. --- docs/sdk/README.md | 4 +- docs/sdk/format-v1.md | 1 + docs/sdk/schema/v1/manifest.schema.json | 45 +++++++++++++++++++ docs/standards/ai-contributions.md | 10 +++-- .../com/mccal/folio/market/PackageManifest.kt | 18 +++++++- .../mccal/folio/market/PackageParserTest.kt | 17 +++++++ .../ParserFuzzTestInputs/manifest/all-fields | 1 + .../conformance/manifest-all-fields.json | 1 + 8 files changed, 90 insertions(+), 7 deletions(-) diff --git a/docs/sdk/README.md b/docs/sdk/README.md index 0814075d..6075bd8a 100644 --- a/docs/sdk/README.md +++ b/docs/sdk/README.md @@ -38,8 +38,8 @@ Make themes, tweaks and layouts for the Folio Market. - **Data only:** JSON and images, plus an optional sandboxed script. No DEX, JAR or native code. - **Declare everything:** list every permission your package uses. The privacy label is built from that list. - **Credit and licensing:** credit anything that inspired you, and don't include GPL code. -- **Made with AI:** start `description` with "AI-assisted (tool name)." and keep that label on updates and forks - ([AI-6](../standards/ai-contributions.md)). +- **Made with AI:** set `aiAssisted` in the manifest, also start `description` with "AI-assisted (tool name).", and keep + both on updates and forks ([AI-6](../standards/ai-contributions.md)). - **Look, read and fit like Folio:** contrast, screens, real screenshots and testing are in the template's [STANDARDS.md](https://github.com/McCal-Codes/folio-source-template/blob/main/STANDARDS.md), drawn from Folio's [standards](../standards/README.md). diff --git a/docs/sdk/format-v1.md b/docs/sdk/format-v1.md index a4ef278c..63a5bd0b 100644 --- a/docs/sdk/format-v1.md +++ b/docs/sdk/format-v1.md @@ -50,6 +50,7 @@ Limits: | `depiction` | path | Usually `depiction.json`. | | `license` | string | SPDX id, e.g. `MIT`. GPL code isn't accepted in the Community source. | | `description` | text | One or two sentences, shown under the name. | +| `aiAssisted` | object | Required if AI helped make the package: `{ "tools": [1 to 5 tool names, 60 characters each], "note"?: text }`. The note says what the AI did. Folio 0.6.6 skips it; see [AI contributions](../standards/ai-contributions.md). | | `provides` *(later)* | array of enum | `iconPack`, `wallpapers`, `widgets`, `folioTheme`: what an external app brings. | | `via` *(later)* | array of object | Required with kind `externalApp`: `{ "store": "playStore"\|"fdroid", "id": … }` or `{ "store": "obtainium", "repoUrl": … }`. | | `requires` | object | `{ "features": [capability ids] }`: the Folio capabilities this package configures. See [Capabilities](#capabilities). | diff --git a/docs/sdk/schema/v1/manifest.schema.json b/docs/sdk/schema/v1/manifest.schema.json index eb0228b3..e69f7c27 100644 --- a/docs/sdk/schema/v1/manifest.schema.json +++ b/docs/sdk/schema/v1/manifest.schema.json @@ -209,6 +209,51 @@ } ] }, + "aiAssisted": { + "description": "Declares that AI helped make this package, and which tools.", + "type": "object", + "required": [ + "tools" + ], + "additionalProperties": false, + "properties": { + "tools": { + "type": "array", + "minItems": 1, + "maxItems": 5, + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "\\S", + "maxLength": 60 + } + }, + "note": { + "oneOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 400 + }, + { + "type": "object", + "required": [ + "en" + ], + "propertyNames": { + "pattern": "^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$" + }, + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 400 + }, + "maxProperties": 64 + } + ] + } + } + }, "via": { "type": "array", "items": { diff --git a/docs/standards/ai-contributions.md b/docs/standards/ai-contributions.md index f35275b6..69ae911c 100644 --- a/docs/standards/ai-contributions.md +++ b/docs/standards/ai-contributions.md @@ -32,8 +32,12 @@ The short version: (found the bug, wrote the test, wrote the fix). The pull request template has a checkbox for this. - **AI-6 MUST** label anything an AI helped make that ships to users (a theme in `themes/`, a tweak, a Market package, an icon pack, a wallpaper, a layout preset) as **AI-assisted**, naming the tool: - - in the package or theme description, in the first line, until the manifest has a field for it (see Gaps); - - in the credits or README that travels with it. + - a Market package sets `aiAssisted` in its manifest: `{ "tools": ["Claude"], "note": "Drafted the colours." }` + ([format](../sdk/format-v1.md)); + - and, until the Market shows that field, also starts `description` with "AI-assisted (tool name)." so every Folio + version shows it (see Gaps); + - a theme in `themes/` starts its description the same way; + - the credits or README that travels with it says so too. - **AI-7 MUST NOT** remove or reword an AI-assisted label when republishing, forking or updating someone else's package. - **AI-8** An unlabelled contribution that turns out to be AI-made may be closed, delisted or removed from a source, @@ -69,6 +73,6 @@ The short version: | # | Work | Size | |---|---|---| -| 1 | An optional `aiAssisted` field in the package manifest (schema v1 addition, `folio-pkg validate`, shown on the Market's package page and at install) | M | +| 1 | Show `aiAssisted` on the Market's package page and at install; then the `description` prefix is no longer needed (the field itself is in schema v1 and the parser) | S | | 2 | The same field in the community theme format (`themes/README.md`, `CommunityThemesTest`) | S | | 3 | A "made with AI" filter in the Market | S | diff --git a/market/src/main/java/com/mccal/folio/market/PackageManifest.kt b/market/src/main/java/com/mccal/folio/market/PackageManifest.kt index afe5fcab..86bc131e 100644 --- a/market/src/main/java/com/mccal/folio/market/PackageManifest.kt +++ b/market/src/main/java/com/mccal/folio/market/PackageManifest.kt @@ -17,12 +17,16 @@ data class PackageManifest( val depiction: String? = null, val license: String? = null, val description: LocalizedText? = null, + val aiAssisted: AiAssisted? = null, val via: List = emptyList(), val provides: Set = emptySet(), val requiredFeatures: Set = emptySet(), ) { data class Author(val name: LocalizedText, val url: String?) + /** `aiAssisted`: the AI tools that helped make the package, and an optional note on what they did. */ + data class AiAssisted(val tools: List, val note: LocalizedText?) + /** Capabilities this package configures that [available] doesn't include ("Needs a newer Folio"). */ fun missingCapabilities(available: Set): Set = requiredFeatures - available @@ -30,13 +34,16 @@ data class PackageManifest( const val MAX_CHARS = 64 * 1024 const val MAX_NAME = 40 const val MAX_RELATIONS = 32 + const val MAX_AI_TOOLS = 5 + const val MAX_AI_TOOL = 60 + private val NOT_BLANK = Regex("\\S") val ID = Regex("^[a-z][a-z0-9-]*(\\.[a-z0-9][a-z0-9-]*)+\\z") const val MAX_ID = 120 private val ANDROID_PACKAGE = Regex("^[A-Za-z][A-Za-z0-9_]*(\\.[A-Za-z][A-Za-z0-9_]*)+\\z") private val KNOWN = setOf( "\$schema", "format", "id", "name", "version", "author", "minFolio", "section", "kind", "permissions", - "screens", "depends", "conflicts", "icon", "depiction", "license", "description", "via", "provides", "requires", + "screens", "depends", "conflicts", "icon", "depiction", "license", "description", "aiAssisted", "via", "provides", "requires", ) fun parse(text: String): ParseResult { @@ -66,6 +73,13 @@ data class PackageManifest( val depiction = f.string("depiction", false, SAFE_PATH, MAX_PATH, "must be a relative path inside the package") val license = f.string("license", false, maxLength = 64) val description = f.text("description", false) + val aiAssisted = f.obj("aiAssisted", false, setOf("tools", "note"))?.let { a -> + val tools = a.strings("tools", true, minItems = 1, maxItems = MAX_AI_TOOLS, unique = true) { value, at -> + a.checkString(value, at, NOT_BLANK, MAX_AI_TOOL, "must name a tool") + } + val note = a.text("note", false) + tools?.let { AiAssisted(it, note) } + } val via = readVia(f) val provides = f.ids("provides", false, Provides::from) val features = f.obj("requires", false, setOf("features"))?.ids("features", false, Capability::from) @@ -76,7 +90,7 @@ data class PackageManifest( kinds = kinds!!.toSet(), permissions = permissions!!.toSet(), screens = screens?.toSet() ?: Screen.entries.toSet(), depends = depends.orEmpty(), conflicts = conflicts.orEmpty(), icon = icon, depiction = depiction, - license = license, description = description, via = via.orEmpty(), + license = license, description = description, aiAssisted = aiAssisted, via = via.orEmpty(), provides = provides.orEmpty().toSet(), requiredFeatures = features.orEmpty().toSet(), ) } diff --git a/market/src/test/java/com/mccal/folio/market/PackageParserTest.kt b/market/src/test/java/com/mccal/folio/market/PackageParserTest.kt index a63b6264..bb2f23d9 100644 --- a/market/src/test/java/com/mccal/folio/market/PackageParserTest.kt +++ b/market/src/test/java/com/mccal/folio/market/PackageParserTest.kt @@ -53,6 +53,23 @@ class PackageParserTest { assertEquals(setOf(Provides.FOLIO_THEME), m.provides) } + @Test fun `reads who says AI helped, and refuses a malformed declaration`() { + val m = ok(PackageManifest.parse(full)) + assertEquals(listOf("Claude", "Example tool"), m.aiAssisted?.tools) + assertEquals("Escribió el primer borrador de los colores.", m.aiAssisted?.note?.resolve(listOf("es"))) + assertNull(ok(PackageManifest.parse(cabinet)).aiAssisted) + assertNull(ok(PackageManifest.parse(edit(cabinet) { put("aiAssisted", JSONObject().put("tools", JSONArray().put("Claude"))) })).aiAssisted?.note) + + fun errors(ai: Any) = manifestErrors(edit(cabinet) { put("aiAssisted", ai) }) + assertTrue(errors(JSONObject()).any { "aiAssisted.tools is required" in it }) + assertTrue(errors(JSONObject().put("tools", JSONArray())).any { "aiAssisted.tools" in it }) + assertTrue(errors(JSONObject().put("tools", JSONArray().put(" "))).any { "aiAssisted.tools[0]" in it }) + assertTrue(errors(JSONObject().put("tools", JSONArray().put("Claude").put("Claude"))).any { "twice" in it }) + assertTrue(errors(JSONObject().put("tools", JSONArray((1..6).map { "Tool $it" }))).any { "more than 5" in it }) + assertTrue(errors(JSONObject().put("tools", JSONArray().put("x".repeat(61)))).any { "longer than 60" in it }) + assertTrue(errors(true).any { "aiAssisted must be an object" in it }) + } + @Test fun `screens default to both`() { val m = ok(PackageManifest.parse(edit(cabinet) { remove("screens") })) assertEquals(setOf(Screen.COVER, Screen.INNER), m.screens) diff --git a/market/src/test/resources/com/mccal/folio/market/ParserFuzzTestInputs/manifest/all-fields b/market/src/test/resources/com/mccal/folio/market/ParserFuzzTestInputs/manifest/all-fields index 492ee46e..15d209b8 100644 --- a/market/src/test/resources/com/mccal/folio/market/ParserFuzzTestInputs/manifest/all-fields +++ b/market/src/test/resources/com/mccal/folio/market/ParserFuzzTestInputs/manifest/all-fields @@ -16,6 +16,7 @@ "depiction": "depiction.json", "license": "MIT", "description": "Parser test input: fills in every manifest field, including the ones no published package uses yet. Not a package anyone can install.", + "aiAssisted": { "tools": ["Claude", "Example tool"], "note": { "en": "Wrote the first draft of the theme colours.", "es": "Escribió el primer borrador de los colores." } }, "via": [{ "store": "obtainium", "repoUrl": "https://github.com/McCal-Codes/folio" }], "provides": ["folioTheme"], "requires": { "features": ["icons.packs", "icons"] } diff --git a/market/src/test/resources/conformance/manifest-all-fields.json b/market/src/test/resources/conformance/manifest-all-fields.json index 492ee46e..15d209b8 100644 --- a/market/src/test/resources/conformance/manifest-all-fields.json +++ b/market/src/test/resources/conformance/manifest-all-fields.json @@ -16,6 +16,7 @@ "depiction": "depiction.json", "license": "MIT", "description": "Parser test input: fills in every manifest field, including the ones no published package uses yet. Not a package anyone can install.", + "aiAssisted": { "tools": ["Claude", "Example tool"], "note": { "en": "Wrote the first draft of the theme colours.", "es": "Escribió el primer borrador de los colores." } }, "via": [{ "store": "obtainium", "repoUrl": "https://github.com/McCal-Codes/folio" }], "provides": ["folioTheme"], "requires": { "features": ["icons.packs", "icons"] }