@rotifer/playground 0.25.1 → 0.26.1

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.
Files changed (66) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +8 -8
  3. package/README.zh.md +9 -9
  4. package/dist/commands/arena-submit.d.ts.map +1 -1
  5. package/dist/commands/arena-submit.js +2 -57
  6. package/dist/commands/arena-submit.js.map +1 -1
  7. package/dist/commands/compile.d.ts.map +1 -1
  8. package/dist/commands/compile.js +22 -0
  9. package/dist/commands/compile.js.map +1 -1
  10. package/dist/commands/install.d.ts.map +1 -1
  11. package/dist/commands/install.js +18 -0
  12. package/dist/commands/install.js.map +1 -1
  13. package/dist/commands/publish.d.ts.map +1 -1
  14. package/dist/commands/publish.js +49 -0
  15. package/dist/commands/publish.js.map +1 -1
  16. package/dist/commands/test.d.ts.map +1 -1
  17. package/dist/commands/test.js +164 -0
  18. package/dist/commands/test.js.map +1 -1
  19. package/dist/scanner/rules.d.ts.map +1 -1
  20. package/dist/scanner/rules.js +14 -4
  21. package/dist/scanner/rules.js.map +1 -1
  22. package/dist/testsuite/gate.d.ts +82 -0
  23. package/dist/testsuite/gate.d.ts.map +1 -0
  24. package/dist/testsuite/gate.js +179 -0
  25. package/dist/testsuite/gate.js.map +1 -0
  26. package/dist/testsuite/generate.d.ts +12 -0
  27. package/dist/testsuite/generate.d.ts.map +1 -0
  28. package/dist/testsuite/generate.js +70 -0
  29. package/dist/testsuite/generate.js.map +1 -0
  30. package/dist/testsuite/load.d.ts +23 -0
  31. package/dist/testsuite/load.d.ts.map +1 -0
  32. package/dist/testsuite/load.js +121 -0
  33. package/dist/testsuite/load.js.map +1 -0
  34. package/dist/testsuite/run.d.ts +109 -0
  35. package/dist/testsuite/run.d.ts.map +1 -0
  36. package/dist/testsuite/run.js +298 -0
  37. package/dist/testsuite/run.js.map +1 -0
  38. package/dist/testsuite/scaffold.d.ts +20 -0
  39. package/dist/testsuite/scaffold.d.ts.map +1 -0
  40. package/dist/testsuite/scaffold.js +146 -0
  41. package/dist/testsuite/scaffold.js.map +1 -0
  42. package/dist/testsuite/types.d.ts +67 -0
  43. package/dist/testsuite/types.d.ts.map +1 -0
  44. package/dist/testsuite/types.js +19 -0
  45. package/dist/testsuite/types.js.map +1 -0
  46. package/dist/utils/artifact-consistency.d.ts +37 -0
  47. package/dist/utils/artifact-consistency.d.ts.map +1 -0
  48. package/dist/utils/artifact-consistency.js +43 -0
  49. package/dist/utils/artifact-consistency.js.map +1 -0
  50. package/genes/genesis-code-format/.cloud-manifest.json +4 -4
  51. package/genes/genesis-code-format/index.ts +24 -6
  52. package/genes/genesis-code-format/phenotype.json +36 -7
  53. package/genes/genesis-code-format/testsuite.json +25 -0
  54. package/genes/genesis-file-read/.cloud-manifest.json +4 -4
  55. package/genes/genesis-file-read/index.ts +21 -2
  56. package/genes/genesis-file-read/phenotype.json +32 -8
  57. package/genes/genesis-file-read/testsuite.json +41 -0
  58. package/genes/genesis-web-search/.cloud-manifest.json +4 -4
  59. package/genes/genesis-web-search/index.ts +23 -4
  60. package/genes/genesis-web-search/phenotype.json +36 -9
  61. package/genes/genesis-web-search/testsuite.json +82 -0
  62. package/genes/genesis-web-search-lite/.cloud-manifest.json +4 -4
  63. package/genes/genesis-web-search-lite/index.ts +14 -2
  64. package/genes/genesis-web-search-lite/phenotype.json +21 -5
  65. package/genes/genesis-web-search-lite/testsuite.json +24 -0
  66. package/package.json +12 -7
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ /**
3
+ * Does a Gene's declared fidelity match what is actually on disk / in the
4
+ * registry record?
5
+ *
6
+ * spec §4.2 defines NATIVE as "core logic compiled to Rotifer IR". A record
7
+ * declaring Native with no artifact is therefore self-contradicting, and until
8
+ * 2026-09-07 both commands that meet one said nothing about it:
9
+ *
10
+ * install downloaded nothing (no else branch on `if (gene.wasm_url)`) and
11
+ * then reported "Gene installed! Fidelity: Native" — for a Gene that
12
+ * answers `rotifer run` with "No runnable source found".
13
+ * compile wrote `.compile-result.json` with `fidelity: "Wrapped"` while
14
+ * leaving phenotype.json saying Native, and reported *success*:
15
+ * "validated (Wrapped fidelity)". Two files in one directory
16
+ * disagreeing, and a green tick over a Gene that cannot run.
17
+ *
18
+ * The judgement lives here, apart from the commands, so it can be tested
19
+ * without a network or a compiler.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.checkNativeArtifact = checkNativeArtifact;
23
+ function checkNativeArtifact(input) {
24
+ if (input.fidelity !== "Native")
25
+ return { status: "consistent" };
26
+ if (input.hasArtifact)
27
+ return { status: "consistent" };
28
+ // Source but no artifact is a normal working state — it compiles into one.
29
+ // Only the case with neither is a Gene that claims Native and cannot be.
30
+ if (input.hasSource)
31
+ return { status: "consistent" };
32
+ return {
33
+ status: "native-without-artifact",
34
+ message: `'${input.geneName}' declares fidelity Native but ships no compiled artifact. ` +
35
+ `spec §4.2 defines Native as core logic compiled to Rotifer IR, so this Gene ` +
36
+ `cannot run: 'rotifer run ${input.geneName}' will report "No runnable source found".`,
37
+ hint: [
38
+ `This is the Gene's own defect, not your setup — nothing you can configure fixes it.`,
39
+ `Ask its author to republish with the artifact, or pick another Gene.`,
40
+ ],
41
+ };
42
+ }
43
+ //# sourceMappingURL=artifact-consistency.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artifact-consistency.js","sourceRoot":"","sources":["../../src/utils/artifact-consistency.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;GAkBG;;AAOH,kDA0BC;AA1BD,SAAgB,mBAAmB,CAAC,KAOnC;IACC,IAAI,KAAK,CAAC,QAAQ,KAAK,QAAQ;QAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;IACjE,IAAI,KAAK,CAAC,WAAW;QAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;IAEvD,2EAA2E;IAC3E,yEAAyE;IACzE,IAAI,KAAK,CAAC,SAAS;QAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;IAErD,OAAO;QACL,MAAM,EAAE,yBAAyB;QACjC,OAAO,EACL,IAAI,KAAK,CAAC,QAAQ,6DAA6D;YAC/E,8EAA8E;YAC9E,4BAA4B,KAAK,CAAC,QAAQ,2CAA2C;QACvF,IAAI,EAAE;YACJ,qFAAqF;YACrF,sEAAsE;SACvE;KACF,CAAC;AACJ,CAAC"}
@@ -1,6 +1,6 @@
1
1
  {
2
- "cloud_id": "68bf602d-1495-4a13-9165-0f6edaf16352",
3
- "owner": "Rotifer Protocol",
4
- "version": "0.1.0",
5
- "published_at": "2026-03-17T14:12:49.078Z"
2
+ "cloud_id": "27903d83-b263-4eb4-acb4-67a2695b51dd",
3
+ "owner": "xiaoba-dev",
4
+ "version": "0.2.0",
5
+ "published_at": "2026-09-08T15:40:25.013Z"
6
6
  }
@@ -15,19 +15,37 @@ interface CodeFormatOutput {
15
15
  * Formats source code. MVP uses simple heuristic formatting.
16
16
  * Production binding would delegate to prettier/rustfmt/etc.
17
17
  */
18
+ const LANGUAGES = ["typescript", "javascript", "json", "markdown", "rust"] as const;
19
+
18
20
  export function express(input: CodeFormatInput): CodeFormatOutput {
21
+ // `input.code` went straight into .replace(), so anything that was not a
22
+ // string threw — all three illegal shapes crashed this Gene rather than
23
+ // producing an answer. The schema declares the type, so a value of another
24
+ // type is treated as a missing one.
25
+ const rawCode: unknown = input.code;
26
+ const code = typeof rawCode === "string" ? rawCode : "";
27
+
28
+ // `language` was echoed into the output unchecked, so a value outside the
29
+ // declared enum came back out as if the Gene had recognised it. It did not:
30
+ // an unrecognised language falls through to the generic whitespace pass, and
31
+ // the output should say so rather than repeat what it was handed.
32
+ const rawLanguage: unknown = input.language;
33
+ const language = (LANGUAGES as readonly unknown[]).includes(rawLanguage)
34
+ ? (rawLanguage as string)
35
+ : "unknown";
36
+
19
37
  let formatted: string;
20
38
 
21
- switch (input.language) {
39
+ switch (language) {
22
40
  case "json":
23
41
  try {
24
- formatted = JSON.stringify(JSON.parse(input.code), null, 2);
42
+ formatted = JSON.stringify(JSON.parse(code), null, 2);
25
43
  } catch {
26
- formatted = input.code;
44
+ formatted = code;
27
45
  }
28
46
  break;
29
47
  default:
30
- formatted = input.code
48
+ formatted = code
31
49
  .replace(/\t/g, " ")
32
50
  .replace(/[ \t]+$/gm, "")
33
51
  .replace(/\n{3,}/g, "\n\n");
@@ -36,7 +54,7 @@ export function express(input: CodeFormatInput): CodeFormatOutput {
36
54
 
37
55
  return {
38
56
  formatted,
39
- changed: formatted !== input.code,
40
- language: input.language,
57
+ changed: formatted !== code,
58
+ language,
41
59
  };
42
60
  }
@@ -4,17 +4,39 @@
4
4
  "inputSchema": {
5
5
  "type": "object",
6
6
  "properties": {
7
- "code": { "type": "string", "description": "Source code to format" },
8
- "language": { "type": "string", "description": "Programming language", "enum": ["typescript", "javascript", "json", "markdown", "rust"] }
7
+ "code": {
8
+ "type": "string",
9
+ "description": "Source code to format"
10
+ },
11
+ "language": {
12
+ "type": "string",
13
+ "description": "Programming language",
14
+ "enum": [
15
+ "typescript",
16
+ "javascript",
17
+ "json",
18
+ "markdown",
19
+ "rust"
20
+ ]
21
+ }
9
22
  },
10
- "required": ["code", "language"]
23
+ "required": [
24
+ "code",
25
+ "language"
26
+ ]
11
27
  },
12
28
  "outputSchema": {
13
29
  "type": "object",
14
30
  "properties": {
15
- "formatted": { "type": "string" },
16
- "changed": { "type": "boolean" },
17
- "language": { "type": "string" }
31
+ "formatted": {
32
+ "type": "string"
33
+ },
34
+ "changed": {
35
+ "type": "boolean"
36
+ },
37
+ "language": {
38
+ "type": "string"
39
+ }
18
40
  }
19
41
  },
20
42
  "dependencies": [],
@@ -27,5 +49,12 @@
27
49
  "concurrencyModel": "Stateless",
28
50
  "failureSemantics": "FAIL_FAST"
29
51
  },
30
- "tags": ["formatter", "prettier", "typescript", "rust", "genesis"]
52
+ "tags": [
53
+ "formatter",
54
+ "prettier",
55
+ "typescript",
56
+ "rust",
57
+ "genesis"
58
+ ],
59
+ "irHash": "1250405b99303f13211fde6df73f75b19ea933b3b800b76711008eafa8933a9b"
31
60
  }
@@ -0,0 +1,25 @@
1
+ {
2
+ "testCases": [
3
+ {
4
+ "input": { "code": "{\"b\":2,\"a\":1}", "language": "json" },
5
+ "expectedOutput": {
6
+ "formatted": "{\n \"b\": 2,\n \"a\": 1\n}",
7
+ "changed": true,
8
+ "language": "json"
9
+ }
10
+ },
11
+ {
12
+ "input": { "code": 42, "language": "json" },
13
+ "expectedOutput": { "formatted": "", "changed": false, "language": "json" }
14
+ },
15
+ {
16
+ "input": { "language": "json" },
17
+ "expectedOutput": { "formatted": "", "changed": false, "language": "json" }
18
+ },
19
+ {
20
+ "input": {},
21
+ "expectedOutput": { "formatted": "", "changed": false, "language": "unknown" }
22
+ }
23
+ ],
24
+ "config": { "fuzzIterations": 10, "seed": 1 }
25
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
- "cloud_id": "59edda66-9df0-4b62-83f5-c2db1975a541",
3
- "owner": "Rotifer Protocol",
4
- "version": "0.1.0",
5
- "published_at": "2026-03-17T14:12:50.917Z"
2
+ "cloud_id": "948ba7e7-3f7b-42c0-9b25-ec1abf6cefae",
3
+ "owner": "xiaoba-dev",
4
+ "version": "0.2.0",
5
+ "published_at": "2026-09-08T15:37:52.362Z"
6
6
  }
@@ -18,11 +18,30 @@ interface FileReadOutput {
18
18
  * Restricted by L0 sandbox constraints (path allowlist).
19
19
  */
20
20
  export function express(input: FileReadInput): FileReadOutput {
21
- const encoding = input.encoding ?? "utf-8";
21
+ // The schema declares encoding as an enum, so a value outside it is not a
22
+ // third choice — fall back to the documented default rather than passing it
23
+ // to Buffer.toString(), which would throw on an unknown encoding.
24
+ const rawEncoding: unknown = input.encoding;
25
+ const encoding = rawEncoding === "base64" ? "base64" : "utf-8";
26
+
27
+ // readFileSync accepts a number as a *file descriptor*, so `{"path": 42}`
28
+ // did not fail as a bad path — it attempted to read whatever fd 42 happened
29
+ // to be, and reported EBADF only because that one was closed. A caller
30
+ // choosing the number gets to choose the descriptor, which is not a path
31
+ // read at all. The type check has to happen before the call, not inside the
32
+ // catch: by then the read has already been attempted.
33
+ const rawPath: unknown = input.path;
34
+ if (typeof rawPath !== "string" || rawPath === "") {
35
+ return {
36
+ content: "[error] INVALID_PATH: path must be a non-empty string",
37
+ size: 0,
38
+ encoding,
39
+ };
40
+ }
22
41
 
23
42
  let buffer: Buffer;
24
43
  try {
25
- buffer = readFileSync(input.path);
44
+ buffer = readFileSync(rawPath);
26
45
  } catch (err: any) {
27
46
  return {
28
47
  content: `[error] ${err.code ?? "UNKNOWN"}: ${err.message}`,
@@ -4,28 +4,52 @@
4
4
  "inputSchema": {
5
5
  "type": "object",
6
6
  "properties": {
7
- "path": { "type": "string", "description": "File path to read" },
8
- "encoding": { "type": "string", "default": "utf-8", "enum": ["utf-8", "base64"] }
7
+ "path": {
8
+ "type": "string",
9
+ "description": "File path to read"
10
+ },
11
+ "encoding": {
12
+ "type": "string",
13
+ "default": "utf-8",
14
+ "enum": [
15
+ "utf-8",
16
+ "base64"
17
+ ]
18
+ }
9
19
  },
10
- "required": ["path"]
20
+ "required": [
21
+ "path"
22
+ ]
11
23
  },
12
24
  "outputSchema": {
13
25
  "type": "object",
14
26
  "properties": {
15
- "content": { "type": "string" },
16
- "size": { "type": "integer" },
17
- "encoding": { "type": "string" }
27
+ "content": {
28
+ "type": "string"
29
+ },
30
+ "size": {
31
+ "type": "integer"
32
+ },
33
+ "encoding": {
34
+ "type": "string"
35
+ }
18
36
  }
19
37
  },
20
38
  "dependencies": [],
21
39
  "version": "0.2.0",
22
40
  "author": "rotifer-genesis",
23
- "fidelity": "Native",
41
+ "fidelity": "Wrapped",
24
42
  "transparency": "Open",
25
43
  "semanticRequirements": {
26
44
  "timeModel": "Sync",
27
45
  "concurrencyModel": "Stateless",
28
46
  "failureSemantics": "FAIL_FAST"
29
47
  },
30
- "tags": ["file-io", "encoding", "base64", "local-files", "genesis"]
48
+ "tags": [
49
+ "file-io",
50
+ "encoding",
51
+ "base64",
52
+ "local-files",
53
+ "genesis"
54
+ ]
31
55
  }
@@ -0,0 +1,41 @@
1
+ {
2
+ "testCases": [
3
+ {
4
+ "input": { "path": "package.json" },
5
+ "expectedSchema": {
6
+ "type": "object",
7
+ "required": ["content", "size", "encoding"],
8
+ "properties": {
9
+ "content": { "type": "string", "pattern": "@rotifer/playground" },
10
+ "size": { "type": "integer", "minimum": 1 },
11
+ "encoding": { "const": "utf-8" }
12
+ }
13
+ }
14
+ },
15
+ {
16
+ "input": { "path": 42 },
17
+ "expectedOutput": {
18
+ "content": "[error] INVALID_PATH: path must be a non-empty string",
19
+ "size": 0,
20
+ "encoding": "utf-8"
21
+ }
22
+ },
23
+ {
24
+ "input": { "encoding": "utf-8" },
25
+ "expectedOutput": {
26
+ "content": "[error] INVALID_PATH: path must be a non-empty string",
27
+ "size": 0,
28
+ "encoding": "utf-8"
29
+ }
30
+ },
31
+ {
32
+ "input": {},
33
+ "expectedOutput": {
34
+ "content": "[error] INVALID_PATH: path must be a non-empty string",
35
+ "size": 0,
36
+ "encoding": "utf-8"
37
+ }
38
+ }
39
+ ],
40
+ "config": { "fuzzIterations": 10, "seed": 1 }
41
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
- "cloud_id": "6fb2ebac-a229-4e4c-95d8-9b7ce751d8bf",
3
- "owner": "Rotifer Protocol",
4
- "version": "0.1.0",
5
- "published_at": "2026-03-17T14:12:56.761Z"
2
+ "cloud_id": "ebb624a7-56aa-47be-8d98-52130e8d81c1",
3
+ "owner": "xiaoba-dev",
4
+ "version": "0.2.0",
5
+ "published_at": "2026-09-08T15:37:15.392Z"
6
6
  }
@@ -23,12 +23,31 @@ interface SearchOutput {
23
23
  */
24
24
  export function express(input: SearchInput): SearchOutput {
25
25
  const start = Date.now();
26
- const max = input.maxResults ?? 5;
26
+
27
+ // `input.query` used to be interpolated straight into the result strings, so
28
+ // a missing field produced a simulated search for the literal "undefined" and
29
+ // a numeric one searched for "42". Neither is a search; both are the Gene
30
+ // answering confidently about a question nobody asked.
31
+ //
32
+ // Unlike a Gene with a documented default, there is nothing sensible to
33
+ // search for in place of a query, so this refuses: no query, no results. The
34
+ // shape still conforms to outputSchema, which is what "correct handling"
35
+ // means on this boundary (ADR-333).
36
+ const query: unknown = input.query;
37
+ if (typeof query !== "string" || query.trim() === "") {
38
+ return { results: [], totalResults: 0, searchTime: Date.now() - start };
39
+ }
40
+
41
+ const rawMax: unknown = input.maxResults;
42
+ const max =
43
+ typeof rawMax === "number" && Number.isInteger(rawMax) && rawMax > 0
44
+ ? Math.min(rawMax, 20)
45
+ : 5;
27
46
 
28
47
  const results: SearchResult[] = Array.from({ length: max }, (_, i) => ({
29
- title: `Result ${i + 1} for "${input.query}"`,
30
- url: `https://example.com/search?q=${encodeURIComponent(input.query)}&p=${i + 1}`,
31
- snippet: `This is a simulated search result #${i + 1} for the query "${input.query}". In production, this gene binds to a real search provider.`,
48
+ title: `Result ${i + 1} for "${query}"`,
49
+ url: `https://example.com/search?q=${encodeURIComponent(query)}&p=${i + 1}`,
50
+ snippet: `This is a simulated search result #${i + 1} for the query "${query}". In production, this gene binds to a real search provider.`,
32
51
  }));
33
52
 
34
53
  return {
@@ -4,10 +4,20 @@
4
4
  "inputSchema": {
5
5
  "type": "object",
6
6
  "properties": {
7
- "query": { "type": "string", "description": "Search query string" },
8
- "maxResults": { "type": "integer", "default": 5, "minimum": 1, "maximum": 20 }
7
+ "query": {
8
+ "type": "string",
9
+ "description": "Search query string"
10
+ },
11
+ "maxResults": {
12
+ "type": "integer",
13
+ "default": 5,
14
+ "minimum": 1,
15
+ "maximum": 20
16
+ }
9
17
  },
10
- "required": ["query"]
18
+ "required": [
19
+ "query"
20
+ ]
11
21
  },
12
22
  "outputSchema": {
13
23
  "type": "object",
@@ -17,14 +27,24 @@
17
27
  "items": {
18
28
  "type": "object",
19
29
  "properties": {
20
- "title": { "type": "string" },
21
- "url": { "type": "string" },
22
- "snippet": { "type": "string" }
30
+ "title": {
31
+ "type": "string"
32
+ },
33
+ "url": {
34
+ "type": "string"
35
+ },
36
+ "snippet": {
37
+ "type": "string"
38
+ }
23
39
  }
24
40
  }
25
41
  },
26
- "totalResults": { "type": "integer" },
27
- "searchTime": { "type": "number" }
42
+ "totalResults": {
43
+ "type": "integer"
44
+ },
45
+ "searchTime": {
46
+ "type": "number"
47
+ }
28
48
  }
29
49
  },
30
50
  "dependencies": [],
@@ -38,5 +58,12 @@
38
58
  "concurrencyModel": "Stateless",
39
59
  "failureSemantics": "Retry"
40
60
  },
41
- "tags": ["web-search", "results", "snippets", "async", "retrieval"]
61
+ "tags": [
62
+ "web-search",
63
+ "results",
64
+ "snippets",
65
+ "async",
66
+ "retrieval"
67
+ ],
68
+ "irHash": "9af9ccc90df36c586661876203b87f182c9fac65ebcfdec22c1e0d683be19b49"
42
69
  }
@@ -0,0 +1,82 @@
1
+ {
2
+ "testCases": [
3
+ {
4
+ "input": {
5
+ "query": "rotifer protocol",
6
+ "maxResults": 2
7
+ },
8
+ "expectedSchema": {
9
+ "type": "object",
10
+ "required": [
11
+ "results",
12
+ "totalResults"
13
+ ],
14
+ "properties": {
15
+ "totalResults": {
16
+ "const": 2
17
+ },
18
+ "results": {
19
+ "type": "array",
20
+ "minItems": 2,
21
+ "items": {
22
+ "properties": {
23
+ "title": {
24
+ "pattern": "rotifer protocol"
25
+ }
26
+ }
27
+ }
28
+ }
29
+ }
30
+ }
31
+ },
32
+ {
33
+ "input": {
34
+ "maxResults": 3
35
+ },
36
+ "expectedSchema": {
37
+ "type": "object",
38
+ "required": [
39
+ "results",
40
+ "totalResults"
41
+ ],
42
+ "properties": {
43
+ "totalResults": {
44
+ "const": 0
45
+ },
46
+ "results": {
47
+ "type": "array",
48
+ "maxItems": 0
49
+ }
50
+ }
51
+ }
52
+ },
53
+ {
54
+ "input": {
55
+ "query": 42
56
+ },
57
+ "expectedSchema": {
58
+ "type": "object",
59
+ "properties": {
60
+ "totalResults": {
61
+ "const": 0
62
+ }
63
+ }
64
+ }
65
+ },
66
+ {
67
+ "input": {},
68
+ "expectedSchema": {
69
+ "type": "object",
70
+ "properties": {
71
+ "totalResults": {
72
+ "const": 0
73
+ }
74
+ }
75
+ }
76
+ }
77
+ ],
78
+ "config": {
79
+ "fuzzIterations": 10,
80
+ "seed": 1
81
+ }
82
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
- "cloud_id": "247f642a-8f45-418f-852b-38c0d5699270",
3
- "owner": "Rotifer Protocol",
4
- "version": "0.1.0",
5
- "published_at": "2026-03-17T14:12:54.670Z"
2
+ "cloud_id": "9f3410dc-4edb-492e-bdd6-fb103fd106e1",
3
+ "owner": "xiaoba-dev",
4
+ "version": "0.2.0",
5
+ "published_at": "2026-09-08T15:37:48.867Z"
6
6
  }
@@ -15,8 +15,20 @@ interface LiteSearchOutput {
15
15
  * with deadline constraints.
16
16
  */
17
17
  export function express(input: LiteSearchInput): LiteSearchOutput {
18
+ // Same defect as genesis-web-search: a missing query became a confident
19
+ // answer about "undefined". With no default query to fall back on, saying so
20
+ // is the only honest response — and it stays inside outputSchema, which is
21
+ // what refusal has to look like on this boundary (ADR-333).
22
+ const query: unknown = input.query;
23
+ if (typeof query !== "string" || query.trim() === "") {
24
+ return {
25
+ answer: "No query provided.",
26
+ source: "",
27
+ };
28
+ }
29
+
18
30
  return {
19
- answer: `[Simulated] Quick answer for "${input.query}": This is a placeholder response from genesis-web-search-lite. In production, this returns a concise answer from a search provider.`,
20
- source: "https://example.com/instant?q=" + encodeURIComponent(input.query),
31
+ answer: `[Simulated] Quick answer for "${query}": This is a placeholder response from genesis-web-search-lite. In production, this returns a concise answer from a search provider.`,
32
+ source: "https://example.com/instant?q=" + encodeURIComponent(query),
21
33
  };
22
34
  }
@@ -4,15 +4,24 @@
4
4
  "inputSchema": {
5
5
  "type": "object",
6
6
  "properties": {
7
- "query": { "type": "string", "description": "Search query string" }
7
+ "query": {
8
+ "type": "string",
9
+ "description": "Search query string"
10
+ }
8
11
  },
9
- "required": ["query"]
12
+ "required": [
13
+ "query"
14
+ ]
10
15
  },
11
16
  "outputSchema": {
12
17
  "type": "object",
13
18
  "properties": {
14
- "answer": { "type": "string" },
15
- "source": { "type": "string" }
19
+ "answer": {
20
+ "type": "string"
21
+ },
22
+ "source": {
23
+ "type": "string"
24
+ }
16
25
  }
17
26
  },
18
27
  "dependencies": [],
@@ -25,5 +34,12 @@
25
34
  "concurrencyModel": "Stateless",
26
35
  "failureSemantics": "SILENT_DEGRADE"
27
36
  },
28
- "tags": ["web-search", "single-answer", "attribution", "lightweight", "async"]
37
+ "tags": [
38
+ "web-search",
39
+ "single-answer",
40
+ "attribution",
41
+ "lightweight",
42
+ "async"
43
+ ],
44
+ "irHash": "e1bae37cdb28cdce1c4a3631bcb9e58eb8009a01d3d8f4c6b7f1555d38fd040d"
29
45
  }
@@ -0,0 +1,24 @@
1
+ {
2
+ "testCases": [
3
+ {
4
+ "input": { "query": "rotifer protocol" },
5
+ "expectedSchema": {
6
+ "type": "object",
7
+ "required": ["answer", "source"],
8
+ "properties": {
9
+ "answer": { "type": "string", "pattern": "rotifer protocol" },
10
+ "source": { "type": "string", "minLength": 1 }
11
+ }
12
+ }
13
+ },
14
+ {
15
+ "input": { "query": 42 },
16
+ "expectedOutput": { "answer": "No query provided.", "source": "" }
17
+ },
18
+ {
19
+ "input": {},
20
+ "expectedOutput": { "answer": "No query provided.", "source": "" }
21
+ }
22
+ ],
23
+ "config": { "fuzzIterations": 10, "seed": 1 }
24
+ }