Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
1 change: 1 addition & 0 deletions docs/sdk/format-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -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). |
Expand Down
45 changes: 45 additions & 0 deletions docs/sdk/schema/v1/manifest.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
10 changes: 7 additions & 3 deletions docs/standards/ai-contributions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 |
18 changes: 16 additions & 2 deletions market/src/main/java/com/mccal/folio/market/PackageManifest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,33 @@ data class PackageManifest(
val depiction: String? = null,
val license: String? = null,
val description: LocalizedText? = null,
val aiAssisted: AiAssisted? = null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Do not land an AI-authored new feature

The commit metadata identifies Codex as the author, and this added model field is part of a new manifest-format feature rather than a bug fix. The repository's pre-existing AI-3 rule (docs/standards/ai-contributions.md:24-26) explicitly forbids using an AI agent for new features and directs agents to open an issue instead, so this change cannot be accepted under the project's contribution policy.

Useful? React with 👍 / 👎.

val via: List<ExternalSource> = emptyList(),
val provides: Set<Provides> = emptySet(),
val requiredFeatures: Set<Capability> = 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<String>, val note: LocalizedText?)

/** Capabilities this package configures that [available] doesn't include ("Needs a newer Folio"). */
fun missingCapabilities(available: Set<Capability>): Set<Capability> = requiredFeatures - available

companion object {
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<PackageManifest> {
Expand Down Expand Up @@ -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)
Expand All @@ -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(),
)
}
Expand Down
17 changes: 17 additions & 0 deletions market/src/test/java/com/mccal/folio/market/PackageParserTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
Loading