Run Form Lens from your own code
Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page scans the Dart - every call matched to its closing bracket, its named arguments read at their own depth - , each class and build method located - for the Form's host and key, every field's validator, the submit's use of validate() and the input basics, then hands it all to the run as facts. The run writes the review or the rewrite; the page then holds it to its own facts and re-scans a rewritten form with the same scanner (formreply.js).
Base URL and the response shape
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response has one shape:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }
The token is minted for this app (the guest call takes {"slug":"form-lens"}), so no slug header is needed afterwards. Send it as Authorization: Bearer YOUR_TOKEN. The run body is the input object, sent bare.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a required key (task, form, code, facts) is missing. The page always sends all five of task, form, code, carry, facts; carry is an empty string for review. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: both lanes are metered and need a personal sign-in. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field comes first
Two lanes, chosen by task, in the order the work runs - inspect, then produce:
review- each of the page's findings confirmed (with the issue, the fix and a short code fix) or cleared (with why), up to five issues a scan cannot see on real lines, and the fixes in the order to make them, each naming its ids.rewrite- the whole rewritten form - a stateful host, one final key bound to the Form, a validator per field that returns a message or null, a submit that waits for validate() - with what changed and what was kept.
A missing or unknown task is answered as review. Both lanes send the same five fields. The facts are the page's scan, one finding per line with its id, and a line starting "must:" is a rule the reply has to hold. The page's handoff carries the review's decisions into carry for the rewrite lane. The review sends at most 24,000 characters of code (the middle of a longer file is cut on whole lines and marked; every finding is still in the facts); the rewrite lane takes at most 16,000.
The input
| Field | Required | What |
|---|---|---|
task | yes | review or rewrite. If it is missing or neither, do review and set "task": "review". |
form | yes | the form's name, the platforms it targets, and the widget class that holds the Form. |
code | yes | the Flutter code of the form. A line starting "// [... N lines cut by the page" marks code the page scanned but did not send. |
carry | send it (empty for review) | rewrite task only: the review's decisions, one per line as id | confirmed or cleared or extra | where | fix or reason; may be empty. |
facts | yes | the page's scan, one finding per line as id | area | severity | line | where | evidence | source, what is present, the submit button and the validator messages it found. A line starting "must:" applies to your reply. |
retry_note | no | optional; if present, your previous reply was unusable - follow it. |
The rules the model applies
From the source skill, @flutter/flutter-building-forms - the only standard the run applies:
- Host the Form in a StatefulWidget.
- Make the GlobalKey<FormState> exactly once, as a final field of the State class (final _formKey = GlobalKey<FormState>();). Never make it inside build - a new key on every rebuild destroys the form's state.
- Pass the key to the Form's key property. In a deep tree where passing it is impractical, a descendant reaches the FormState with Form.of(context).
- Every input is a TextFormField (a TextField wrapped in a FormField) inside the Form, with a validator.
- A validator returns a String with the specific error message when the value is invalid, and null when it is valid. Any String - even an empty one - is an error.
- The submit button's onPressed calls _formKey.currentState!.validate(). When it is true, every validator returned null: submit and show a success indicator such as a SnackBar. When it is false, the Form shows the messages under the fields; nothing is submitted.
- The page's additions (not the skill's): a password field sets obscureText: true; an email field sets keyboardType: TextInputType.emailAddress and a phone field TextInputType.phone; a TextEditingController is made once in the State class and disposed in dispose().
House rules
- Use only the input. Never invent a field, a rule, a message or a string the code does not hold. When a new validator needs a message, write a short one in the style of the form's existing messages and name it in your fix or change.
- The page's ids and lines are measured facts: never renumber an id.
- No web links (http or https) in prose.
- Write in English. Keep the JSON keys and the ids exactly as given.
- Never use backticks. Name classes and parameters plainly (TextFormField, validator:). Words from the rules above are free to use; what you never do is paste a whole sentence from this prompt.
- Never write a gendered pronoun for a user; write they, the user or the person.
- Reply with ONLY one JSON object - no prose before or after, no code fences.
The replies
review
Confirm or clear each of the page's findings, add what a scan cannot see, and put the fixes in order.
summary- one sentence, at most 40 words: what a user of this form meets today, and the first fix.findings- one object per page finding you confirm, in id order:id(the page's id),widget(where it is, e.g. "TextFormField - the email field"),issue(one sentence: what goes wrong for the person filling the form - it never submits, it submits a blank address, it loses what was typed),fix(one sentence: the change),code(the corrected Dart for that piece only, at most 6 lines; "" when fix says it all).cleared- one object per page finding that is not a problem in this code, withidandwhy(one sentence tied to the code). Every page id appears exactly once, in findings or in cleared.extra- at most 5 issues the scan cannot see, each tied to a line of the code:id(X1, X2, ...),area(architecture, validation, submit, input or lifecycle),line(a number),widget,issue,fix. Examples: a validator whose rule does not fit its field (an email checked only for "@"), a submit that runs twice on a double tap, no success indicator after a valid submit, a value read from the controller before validate().[]when there are none.plan- 2 to 10 short strings, the fixes in the order to make them (what stops the form working first - the key, then validate(), then the validators, then input basics), each naming the ids it covers (e.g. "Move the GlobalKey out of build into a final field (F1)"). Every confirmed id appears in a step.[]only when nothing is confirmed.caveats- at most 4 short strings;[]when there is nothing to say.
{
"task": "review",
"summary": "one sentence",
"findings": [ { "id": "F1", "widget": "GlobalKey<FormState> - the form key", "issue": "one sentence", "fix": "one sentence", "code": "final _formKey = GlobalKey<FormState>();" } ],
"cleared": [ { "id": "F4", "why": "one sentence" } ],
"extra": [ { "id": "X1", "area": "validation", "line": 18, "widget": "TextFormField - the email field", "issue": "one sentence", "fix": "one sentence" } ],
"plan": [ "Move the GlobalKey out of build into a final field (F1)" ],
"caveats": [ ]
}
The page holds it to its own facts (check() in formreply.js; any warning makes the reply flagged): the reply answers the task it was sent; every page finding id appears exactly once across findings and cleared, and no id the page never found; a cleared id has a reason; a confirmed id has an issue and a fix; at most five extra issues, each on a line inside the paste; a fix order of 2 to 10 steps that names every confirmed id; a summary; no web link in the prose.
rewrite
Rewrite the file with the fixes applied.
summary- one sentence, at most 40 words: what the rewrite changes and what it leaves for the team.code- the complete rewritten file. Keep every import, class, constructor parameter, callback and behaviour, and keep every validator message as worded; change only what the findings name. Never shorten the file with "..." or "unchanged" - write every line. If the code holds a line starting "// [... N lines cut by the page", keep that line exactly where it is. A Form in a StatelessWidget - make the widget a StatefulWidget with a State class (keep the widget's name and constructor), and move the key, the controllers and build into the State. A key made in build, or not final - final _formKey = GlobalKey<FormState>(); as a field of the State class, and key: _formKey on the Form. A TextField in the Form - a TextFormField with the same controller and decoration, and a validator. A missing validator - one that returns a short message for an empty value (in the style of the form's messages) and null otherwise. A validator that returns '' - return null on success instead. A validator with no empty check - check value == null || value.trim().isEmpty first. validate() missing or ignored - if (_formKey.currentState!.validate()) { the existing submission }. A visible password - obscureText: true. A missing keyboard - the keyboardType the finding names. A controller never disposed - a dispose() override that disposes every controller, then super.dispose().changes- one object per page finding you fixed, in id order:id,change(one sentence: what changed, naming the widget). Extra ids (X1, ...) from the carried review may appear too.kept- one object per page finding you did not change:id,why(one sentence - the review cleared it, or it needs a decision the code cannot make). Every page id appears exactly once, in changes or in kept; an id the carried review cleared belongs in kept.caveats- at most 4 short strings;[]when there is nothing to say.
{
"task": "rewrite",
"summary": "one sentence",
"code": "the complete rewritten Dart file",
"changes": [ { "id": "F1", "change": "one sentence" } ],
"kept": [ { "id": "F4", "why": "one sentence" } ],
"caveats": [ ]
}
The page requires: the reply answers the task it was sent; every page finding id appears exactly once across changes and kept; every widget class of the paste survives; the file is not shortened (at least 70% of its lines) and elides nothing; and the rewritten code is scanned again - a rule the changes claim fixed that still fires, or a rule the paste never had, is flagged. A validator message the rewrite drops is noted.
Worked examples
Three runs from the page's free samples (invented apps): the review of a sign-in form that can never pass, the rewrite of the same form carrying that review, and the review of a checkout address form that skips validation. Each input is exactly what the page sends; each reply is the saved reply the sample replays, shipped unedited - but the code, the facts and long strings are shortened in this display only, and every cut is listed under the example. The last line is the page's own check of the full reply.
review - A sign-in form that can never pass
The page read it as: Validation is broken - 2 blocker, 3 major, 1 minor; findings F1 key-in-build, F2 controller-in-build, F3 form-stateless, F4 keyboard-type, F5 validator-empty-string, F6 password-visible.
Input (facts is 1691 characters; the first 1390 are shown):
{
"task": "review",
"form": "Form: Sign in\nTargets: Android and iOS\nForm widget: SignInForm",
"code": "import 'package:flutter/material.dart';\n\nclass SignInForm extends StatelessWidget {\n const SignInForm({super.key});\n\n @override\n Widget build(BuildContext context) {\n final formKey = GlobalKey<FormState>();\n final email = TextEditingController();\n return Form(\n key: formKey,\n child: Column(\n children: [\n TextFormField(\n controller: email,\n decoration: const InputDecoration(labelText: 'Email'),\n validator: (value) {\n if (value == null || !value.contains('@')) {\n return 'Enter a valid email';\n }\n return '';\n },\n ),\n TextFormField(\n decoration: const InputDecoration(labelText: 'Password'),\n validator: (value) => value == null || value.length < 8 ? 'Use at least 8 characters' : null,\n ),\n FilledButton(\n onPressed: () {\n if (formKey.currentState!.validate()) {\n ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Signing in')));\n }\n },\n child: const Text('Sign in'),\n ),\n ],\n ),\n );\n }\n}\n",
"carry": "",
"facts": "The page's scan of 41 lines (14 calls); verdict: Validation is broken - 2 blocker, 3 major, 1 minor.\nFindings (id | area | severity | line | where | evidence | source):\n- F1 | architecture | blocker | line 8 | - | GlobalKey<FormState>() is created inside build, so every rebuild makes a new key and the form loses its state - typed text and errors vanish. | source: final formKey = GlobalKey<FormState>();\n- F2 | lifecycle | major | line 9 | - | TextEditingController email is created inside build, so each rebuild makes a new one and the typed text is lost. | source: final email = TextEditingController();\n- F3 | architecture | major | line 10 | Form | The Form is built in SignInForm, a StatelessWidget - the skill hosts a Form in a StatefulWidget so its key and field state survive rebuilds. | source: return Form(\n- F4 | input | minor | line 14 | TextFormField | The Email field opens the default keyboard - it has no keyboardType. | source: TextFormField(\n- F5 | validation | blocker | line 17 | TextFormField | The validator returns an empty string - any String, even '', counts as an error, so validate() is never true. | source: validator: (value) {\n- F6 | input | major | line 24 | TextFormField | The Password field shows what is typed - it has no obscureText: true. | source: TextFormField(\nPresent: 1 Form, 2 form fields, 2 validators, 1 TextEditingController.\nThe form widget:\n\n[... 301 more characters cut from this display ...]"
}
Reply:
{
"task": "review",
"summary": "A user can never sign in today: the email validator returns an empty string on success, so validate() is always false, and the key and controller are rebuilt inside build; first move the form into a StatefulWidget with a final key.",
"findings": [
{
"id": "F1",
"widget": "GlobalKey<FormState> - the form key",
"issue": "A new key is made on every rebuild, so the person's typed text and the error messages under the fields vanish whenever the widget rebuilds.",
"fix": "Make the key once as a final field of the new State class and bind it with key: _formKey on the Form.",
"code": "final _formKey = GlobalKey<FormState>();"
},
{
"id": "F2",
"widget": "TextEditingController - the email controller",
"issue": "A fresh controller is made on every rebuild, so the address the user typed is dropped and the old controllers are never disposed.",
"fix": "Make the controller once as a final field of the State class and dispose it in a dispose() override before super.dispose().",
"code": "final _email = TextEditingController();\n\n@override\nvoid dispose() {\n _email.dispose();\n super.dispose();\n}"
},
{
"id": "F3",
"widget": "SignInForm - the StatelessWidget that builds the Form",
"issue": "A StatelessWidget has nowhere to keep the key and the controller, so the form state cannot survive a rebuild.",
"fix": "Turn SignInForm into a StatefulWidget with a State class that holds the key, the controller and build, keeping the const constructor.",
"code": "class SignInForm extends StatefulWidget {\n const SignInForm({super.key});\n\n @override\n State<SignInForm> createState() => _SignInFormState();\n}"
},
{
"id": "F4",
"widget": "TextFormField - the email field",
"issue": "The user gets the default keyboard and has to hunt for the @ key while typing an address.",
"fix": "Add keyboardType: TextInputType.emailAddress to the email TextFormField.",
"code": "keyboardType: TextInputType.emailAddress,"
},
{
"id": "F5",
"widget": "TextFormField - the email validator",
"issue": "On a valid address the validator returns an empty String, which counts as an error, so validate() is never true and the form never signs in.",
"fix": "Return null instead of the empty String when the address passes.",
"code": "return null;"
},
{
"id": "F6",
"widget": "TextFormField - the password field",
"issue": "The password is shown in plain text as the person types it, so anyone nearby can read it.",
"fix": "Add obscureText: true to the password TextFormField.",
"code": "obscureText: true,"
}
],
"cleared": [],
"extra": [
{
"id": "X1",
"area": "validation",
"line": 18,
"widget": "TextFormField - the email validator",
"issue": "The rule only looks for an @, so an entry such as a lone @ or an address with spaces passes as valid.",
"fix": "Check value == null || value.trim().isEmpty first, then require some text before and after the @ and a dot in the part after it."
},
{
"id": "X2",
"area": "validation",
"line": 26,
"widget": "TextFormField - the password validator",
"issue": "Eight spaces pass the length check, so a blank-looking password is accepted.",
"fix": "Return 'Enter a password' when value.trim().isEmpty before the length check."
},
{
"id": "X3",
"area": "submit",
"line": 31,
"widget": "FilledButton - the Sign in button",
"issue": "A valid submit only shows the Signing in SnackBar; nothing reads the email or password or hands them to a sign-in call.",
"fix": "Inside the validate() branch, pass the controller text and the password to the app's sign-in callback, then show the SnackBar."
},
{
"id": "X4",
"area": "submit",
"line": 29,
"widget": "FilledButton - the Sign in button",
"issue": "A double tap runs the submit twice and stacks two SnackBars, and would send two sign-in requests once one is wired.",
"fix": "Keep a submitting flag in the State class and set onPressed to null while a sign-in is in progress."
}
],
"plan": [
"Turn SignInForm into a StatefulWidget with a State class (F3)",
"Move the GlobalKey out of build into a final field bound to the Form (F1)",
"Move the email TextEditingController into the State class and dispose it (F2)",
"Return null from the email validator on success so validate() can pass (F5)",
"Tighten the email and password validators with empty checks (X1, X2)",
"Wire the valid submit to a real sign-in and guard against double taps (X3, X4)",
"Hide the password with obscureText: true and give the email field its keyboard (F6, F4)"
],
"caveats": [
"The code holds no sign-in callback, so X3 needs the team to name where the credentials go.",
"The password field has no controller, so the submit needs one or an onSaved to read the value."
]
}
What step 5 prints for it: F1: Make the key once as a final field of the new State class and bind it with key: _formKey on the Form. | F2: Make the controller once as a final field of the State class and dispose it in a dispose() override before super.dispose(). | F3: Turn SignInForm into a StatefulWidget with a State class that holds the key, the controller and build, keeping the const constructor. | F4: Add keyboardType: TextInputType.emailAddress to the email TextFormField. | F5: Return null instead of the empty String when the address passes. | F6: Add obscureText: true to the password TextFormField.
The page's check: kept - nothing to flag
rewrite - A sign-in form that can never pass
The input carries the saved review (R.laneForm fills carry from the review reply), as the page's handoff does. The page re-scanned the rewritten code: 6 findings before, 0 after.
Input (facts is 1691 characters; the first 1390 are shown; carry is 1578 characters; the first 1395 are shown):
{
"task": "rewrite",
"form": "Form: Sign in\nTargets: Android and iOS\nForm widget: SignInForm",
"code": "import 'package:flutter/material.dart';\n\nclass SignInForm extends StatelessWidget {\n const SignInForm({super.key});\n\n @override\n Widget build(BuildContext context) {\n final formKey = GlobalKey<FormState>();\n final email = TextEditingController();\n return Form(\n key: formKey,\n child: Column(\n children: [\n TextFormField(\n controller: email,\n decoration: const InputDecoration(labelText: 'Email'),\n validator: (value) {\n if (value == null || !value.contains('@')) {\n return 'Enter a valid email';\n }\n return '';\n },\n ),\n TextFormField(\n decoration: const InputDecoration(labelText: 'Password'),\n validator: (value) => value == null || value.length < 8 ? 'Use at least 8 characters' : null,\n ),\n FilledButton(\n onPressed: () {\n if (formKey.currentState!.validate()) {\n ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Signing in')));\n }\n },\n child: const Text('Sign in'),\n ),\n ],\n ),\n );\n }\n}\n",
"carry": "F1 | confirmed | GlobalKey<FormState> - the form key | Make the key once as a final field of the new State class and bind it with key: _formKey on the Form.\nF2 | confirmed | TextEditingController - the email controller | Make the controller once as a final field of the State class and dispose it in a dispose() override before super.dispose().\nF3 | confirmed | SignInForm - the StatelessWidget that builds the Form | Turn SignInForm into a StatefulWidget with a State class that holds the key, the controller and build, keeping the const constructor.\nF4 | confirmed | TextFormField - the email field | Add keyboardType: TextInputType.emailAddress to the email TextFormField.\nF5 | confirmed | TextFormField - the email validator | Return null instead of the empty String when the address passes.\nF6 | confirmed | TextFormField - the password field | Add obscureText: true to the password TextFormField.\nX1 | extra | TextFormField - the email validator (line 18) | Check value == null || value.trim().isEmpty first, then require some text before and after the @ and a dot in the part after it.\nX2 | extra | TextFormField - the password validator (line 26) | Return 'Enter a password' when value.trim().isEmpty before the length check.\nX3 | extra | FilledButton - the Sign in button (line 31) | Inside the validate() branch, pass the controller text and the password to the app's sign-in callback,\n\n[... 183 more characters cut from this display ...]",
"facts": "The page's scan of 41 lines (14 calls); verdict: Validation is broken - 2 blocker, 3 major, 1 minor.\nFindings (id | area | severity | line | where | evidence | source):\n- F1 | architecture | blocker | line 8 | - | GlobalKey<FormState>() is created inside build, so every rebuild makes a new key and the form loses its state - typed text and errors vanish. | source: final formKey = GlobalKey<FormState>();\n- F2 | lifecycle | major | line 9 | - | TextEditingController email is created inside build, so each rebuild makes a new one and the typed text is lost. | source: final email = TextEditingController();\n- F3 | architecture | major | line 10 | Form | The Form is built in SignInForm, a StatelessWidget - the skill hosts a Form in a StatefulWidget so its key and field state survive rebuilds. | source: return Form(\n- F4 | input | minor | line 14 | TextFormField | The Email field opens the default keyboard - it has no keyboardType. | source: TextFormField(\n- F5 | validation | blocker | line 17 | TextFormField | The validator returns an empty string - any String, even '', counts as an error, so validate() is never true. | source: validator: (value) {\n- F6 | input | major | line 24 | TextFormField | The Password field shows what is typed - it has no obscureText: true. | source: TextFormField(\nPresent: 1 Form, 2 form fields, 2 validators, 1 TextEditingController.\nThe form widget:\n\n[... 301 more characters cut from this display ...]"
}
Reply (long strings shortened for display - see the cuts below):
{
"task": "rewrite",
"summary": "SignInForm becomes a StatefulWidget whose State holds one final form key and a disposed email controller, the email validator returns null on success, and the fields get their keyboard and hidden password; the sign-in call is left for the team.",
"code": "import 'package:flutter/material.dart';\n\nclass SignInForm extends StatefulWidget {\n const SignInForm({super.key});\n\n @override\n State<SignInForm> createState() => _SignInFormState();\n}\n\nclass _SignInFormState extends State<SignInForm> {\n final _formKey = GlobalKey<FormState>();\n final _email = [... 1577 more characters cut from this display]",
"changes": [
{
"id": "F1",
"change": "The GlobalKey<FormState> is now made once as final _formKey in _SignInFormState and bound with key: _formKey on the Form."
},
{
"id": "F2",
"change": "The email TextEditingController is now the final field _email of the State class and is disposed in a dispose() override before super.dispose()."
},
{
"id": "F3",
"change": "SignInForm is now a StatefulWidget with the same const constructor, and _SignInFormState holds the key, the controller and build."
},
{
"id": "F4",
"change": "The email TextFormField now sets keyboardType: TextInputType.emailAddress."
},
{
"id": "F5",
"change": "The email validator in the TextFormField now returns null when the address passes instead of the empty String."
},
{
"id": "F6",
"change": "The password TextFormField now sets obscureText: true."
},
{
"id": "X1",
"change": "The email validator checks for an empty value first, then requires text before the @ and a dot after it, still returning 'Enter a valid email'."
},
{
"id": "X2",
"change": "The password validator returns the new message 'Enter a password' for an empty value before the length check."
}
],
"kept": [],
"caveats": [
"X3 is not applied: SignInForm has no sign-in callback to pass the email and password to, so the team must add one and call it inside the validate() branch.",
"X4 is not applied: the button only shows a SnackBar today, so a submitting flag waits for a real asynchronous sign-in call.",
"'Enter a password' is a new message written in the style of the existing ones."
]
}
Cut from this display only (the saved reply and the check use the full text): code (1876 characters, 299 shown).
What step 5 prints for it: F1: The GlobalKey<FormState> is now made once as final _formKey in _SignInFormState and bound with key: _formKey on the Form. | F2: The email TextEditingController is now the final field _email of the State class and is disposed in a dispose() override before super.dispose(). | F3: SignInForm is now a StatefulWidget with the same const constructor, and _SignInFormState holds the key, the controller and build. | F4: The email TextFormField now sets keyboardType: TextInputType.emailAddress. | F5: The email validator in the TextFormField now returns null when the address passes instead of the empty String. | F6: The password TextFormField now sets obscureText: true. | X1: The email validator checks for an empty value first, then requires text before the @ and a dot after it, still returning 'Enter a valid email'. | X2: The password validator returns the new message 'Enter a password' for an empty value before the length check.
The page's check: kept - nothing to flag
review - A checkout address form that skips validation
The page read it as: Fix before release - 0 blocker, 6 major, 1 minor; findings F1 key-not-final, F2 controller-not-disposed, F3 controller-not-disposed, F4 controller-not-disposed, F5 textfield-in-form, F6 field-no-validator, F7 validate-ignored.
Input (code is 1456 characters; the first 1398 are shown; facts is 1747 characters; the first 1398 are shown):
{
"task": "review",
"form": "Form: Shipping address\nTargets: Android and iOS\nForm widget: AddressForm",
"code": "import 'package:flutter/material.dart';\n\nclass AddressForm extends StatefulWidget {\n const AddressForm({super.key, required this.onSaved});\n final void Function(String name, String street, String zip) onSaved;\n\n @override\n State<AddressForm> createState() => _AddressFormState();\n}\n\nclass _AddressFormState extends State<AddressForm> {\n GlobalKey<FormState> _formKey = GlobalKey<FormState>();\n final _name = TextEditingController();\n final _street = TextEditingController();\n final _zip = TextEditingController();\n\n @override\n Widget build(BuildContext context) {\n return Form(\n key: _formKey,\n child: Column(\n children: [\n TextFormField(\n controller: _name,\n decoration: const InputDecoration(labelText: 'Full name'),\n validator: (value) => value == null || value.trim().isEmpty ? 'Enter your name' : null,\n ),\n TextField(\n controller: _street,\n decoration: const InputDecoration(labelText: 'Street address'),\n ),\n TextFormField(\n controller: _zip,\n decoration: const InputDecoration(labelText: 'Postcode'),\n ),\n ElevatedButton(\n onPressed: () {\n _formKey.currentState!.validate();\n widget.onSaved(_name.text, _street.text, _zip.text);\n },\n child: const Text('Use this\n\n[... 58 more characters cut from this display ...]",
"carry": "",
"facts": "The page's scan of 48 lines (16 calls); verdict: Fix before release - 0 blocker, 6 major, 1 minor.\nFindings (id | area | severity | line | where | evidence | source):\n- F1 | architecture | minor | line 12 | - | The GlobalKey<FormState> field is not final, so it can be replaced and the form's state lost. | source: GlobalKey<FormState> _formKey = GlobalKey<FormState>();\n- F2 | lifecycle | major | line 13 | - | TextEditingController _name is never disposed, so it leaks when the form closes. | source: final _name = TextEditingController();\n- F3 | lifecycle | major | line 14 | - | TextEditingController _street is never disposed, so it leaks when the form closes. | source: final _street = TextEditingController();\n- F4 | lifecycle | major | line 15 | - | TextEditingController _zip is never disposed, so it leaks when the form closes. | source: final _zip = TextEditingController();\n- F5 | validation | major | line 28 | TextField | A plain TextField sits inside the Form - it has no validator, so validate() never checks it. | source: TextField(\n- F6 | validation | major | line 32 | TextFormField | TextFormField has no validator:, so the form accepts anything in it. | source: TextFormField(\n- F7 | submit | major | line 38 | - | validate() is called but its result is ignored, so the submission runs even when a field is invalid. | source: _formKey.currentState!.validate();\nPresent: 1 Form,\n\n[... 349 more characters cut from this display ...]"
}
Reply:
{
"task": "review",
"summary": "Today a person can tap Use this address with a blank street and postcode and the address is still saved, because the validate() result is ignored; first, submit only when validate() returns true.",
"findings": [
{
"id": "F1",
"widget": "GlobalKey<FormState> - the form key",
"issue": "The key field can be reassigned, and a new key would drop what the person has typed into the form.",
"fix": "Declare the key as a final field of _AddressFormState.",
"code": "final _formKey = GlobalKey<FormState>();"
},
{
"id": "F2",
"widget": "TextEditingController - the name controller",
"issue": "The _name controller is never disposed, so it stays in memory after the person leaves the address screen.",
"fix": "Add a dispose() override that disposes _name before super.dispose().",
"code": "@override\nvoid dispose() {\n _name.dispose();\n super.dispose();\n}"
},
{
"id": "F3",
"widget": "TextEditingController - the street controller",
"issue": "The _street controller is never disposed, so it stays in memory after the person leaves the address screen.",
"fix": "Dispose _street in the same dispose() override.",
"code": "_street.dispose();"
},
{
"id": "F4",
"widget": "TextEditingController - the postcode controller",
"issue": "The _zip controller is never disposed, so it stays in memory after the person leaves the address screen.",
"fix": "Dispose _zip in the same dispose() override.",
"code": "_zip.dispose();"
},
{
"id": "F5",
"widget": "TextField - the street address field",
"issue": "The street field is a plain TextField with no validator, so the form saves an address with a blank street.",
"fix": "Replace it with a TextFormField that keeps the controller and decoration and adds a validator with the new message 'Enter your street address'.",
"code": "TextFormField(\n controller: _street,\n decoration: const InputDecoration(labelText: 'Street address'),\n validator: (value) => value == null || value.trim().isEmpty ? 'Enter your street address' : null,\n),"
},
{
"id": "F6",
"widget": "TextFormField - the postcode field",
"issue": "The postcode field has no validator, so the form accepts a blank postcode.",
"fix": "Add a validator with the new message 'Enter your postcode' that returns null when a value is present.",
"code": "validator: (value) => value == null || value.trim().isEmpty ? 'Enter your postcode' : null,"
},
{
"id": "F7",
"widget": "ElevatedButton - the Use this address button",
"issue": "The validate() result is thrown away, so onSaved runs and the address is saved even while the name field shows its error.",
"fix": "Call widget.onSaved only inside an if on _formKey.currentState!.validate().",
"code": "if (_formKey.currentState!.validate()) {\n widget.onSaved(_name.text, _street.text, _zip.text);\n}"
}
],
"cleared": [],
"extra": [
{
"id": "X1",
"area": "submit",
"line": 39,
"widget": "ElevatedButton - the Use this address button",
"issue": "After a valid submit nothing on screen tells the person the address was accepted.",
"fix": "After widget.onSaved, show a SnackBar through ScaffoldMessenger.of(context).showSnackBar with a short confirmation."
},
{
"id": "X2",
"area": "submit",
"line": 37,
"widget": "ElevatedButton - the Use this address button",
"issue": "A double tap calls onSaved twice, so the same address can be saved twice.",
"fix": "Keep a _submitting flag in the State, set it in setState before calling onSaved, and set onPressed to null while it is true."
}
],
"plan": [
"Make the form key a final field of _AddressFormState (F1)",
"Wrap widget.onSaved in an if on _formKey.currentState!.validate() (F7)",
"Turn the street TextField into a TextFormField with a validator (F5)",
"Add a validator to the postcode TextFormField (F6)",
"Add a dispose() override for _name, _street and _zip (F2, F3, F4)",
"Show a SnackBar after a valid submit and block a second tap while saving (X1, X2)"
],
"caveats": [
"The postcode validator only checks for a blank value; a format rule depends on the countries the app ships to."
]
}
What step 5 prints for it: F1: Declare the key as a final field of _AddressFormState. | F2: Add a dispose() override that disposes _name before super.dispose(). | F3: Dispose _street in the same dispose() override. | F4: Dispose _zip in the same dispose() override. | F5: Replace it with a TextFormField that keeps the controller and decoration and adds a validator with the new message 'Enter your street address'. | F6: Add a validator with the new message 'Enter your postcode' that returns null when a value is present. | F7: Call widget.onSaved only inside an if on _formKey.currentState!.validate().
The page's check: kept - nothing to flag
Build the input yourself
Load the engine in node (global.window = global; require("./formkit.js"); require("./formreply.js")), then:
const K = FormKit, R = FormReply;
const f = K.form({ name: "Login", targets: "mobile", carry: "",
code: "class Login extends StatelessWidget {\n Widget build(BuildContext c) {\n final key = GlobalKey<FormState>();\n return Form(key: key, child: TextFormField());\n }\n}" });
// f.findings (id, area, sev, line, widget, evidence, fix), f.checklist, f.test (the empty-submit widget test)
// K.findingsCsv(f), K.checklistMd(f) - the free exports
const b = K.buildInput(f, "review"); // or "rewrite" (R.laneForm(form, "rewrite", review) fills carry)
const wire = K.toWire(b.input); // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "review");
// R.check(reply, b) // {status: kept or flags, notes}
Get a token for the steps below from /tokens.html.
Step 1 — a tiny client helper
One function that sends JSON and unwraps the response. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="YOUR_TOKEN" # from https://form-lens.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from https://form-lens.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// form-lens.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // from https://form-lens.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "YOUR_TOKEN" // from https://form-lens.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// FormLens.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class FormLens {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "YOUR_TOKEN"; // from https://form-lens.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from https://form-lens.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "YOUR_TOKEN"; // from https://form-lens.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "YOUR_TOKEN"; // from https://form-lens.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
Step 2 — a token
A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"form-lens"}' | jq -r '.data.token')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "form-lens"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "form-lens" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "form-lens"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "form-lens"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "form-lens" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "form-lens"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "form-lens" }))!["token"]!.GetValue<string>();
Step 3 — GET /me
Who the token is (subject_type: user or guest) and the credit balance.
api GET /me | jq '.data | {subject_type, credits}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["credits"]}");
Step 4 — POST /estimate
Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.
# input.json: the run input object - {"task": "review", "form": "Form: Sign in
Targets: Android and iOS
Form widget: SignInForm", "code": "import 'package:flutter/material.dart';
...", "carry": "", "facts": "The page's scan of 40 lines ...
Findings (id | area | severity | line | where | evidence | source):
- F1 | architecture | blocker | line 8 | ..."}
# - see the input table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "review", "form": "Form: Sign in
Targets: Android and iOS
Form widget: SignInForm", "code": "import 'package:flutter/material.dart';
...", "carry": "", "facts": "The page's scan of 40 lines ...
Findings (id | area | severity | line | where | evidence | source):
- F1 | architecture | blocker | line 8 | ..."}
# - see the input table
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "review" or "rewrite", form, code, carry, facts} (see the input table)
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
Step 5 — POST /run, then poll the job
Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses form-lens:<task>:<inputHash>:a<attempt> - the lane is part of the key, retry_note is left out of the hash - and its one automatic reformat retry is a new attempt number, named in the page as one extra run. A review reply carries summary, findings, cleared, extra, plan and caveats; a rewrite reply carries summary, code, changes, kept and caveats. The code below prints the gist of either: each confirmed finding and its fix, or each change.
KEY="form-lens:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
jq -r 'if .task == "rewrite" then (.changes[] | "\(.id): \(.change)")
else (.findings[] | "\(.id): \(.fix)") end' reply.json
import hashlib, time
key = "form-lens:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply["task"])
if reply["task"] == "rewrite":
for x in reply["changes"]:
print(x["id"], x["change"])
else:
for x in reply["findings"]:
print(x["id"], x["fix"])
import { createHash } from "node:crypto";
const key = `form-lens:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.task);
if (reply.task === "rewrite") { for (const x of reply.changes) console.log(x.id, x.change); }
else { for (const x of reply.findings) console.log(x.id, x.fix); }
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "form-lens:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
var reply map[string]any
json.Unmarshal([]byte(j["output"].(map[string]any)["output"].(string)), &reply)
fmt.Println(j["charged_credits"], j["truncated"], reply["task"])
key, b := "findings", "fix"
if reply["task"] == "rewrite" {
key, b = "changes", "change"
}
xs, _ := reply[key].([]any)
for _, x := range xs {
xx := x.(map[string]any)
fmt.Println(xx["id"], xx[b])
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "form-lens:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply.path("task").asText());
if (reply.path("task").asText().equals("rewrite")) {
for (JsonNode x : reply.path("changes")) System.out.println(x.path("id").asText() + " " + x.path("change").asText());
} else {
for (JsonNode x : reply.path("findings")) System.out.println(x.path("id").asText() + " " + x.path("fix").asText());
}
require "digest"
key = "form-lens:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["task"]].inspect
if reply["task"] == "rewrite"
reply["changes"].each { |x| puts "#{x["id"]} #{x["change"]}" }
else
reply["findings"].each { |x| puts "#{x["id"]} #{x["fix"]}" }
end
$key = "form-lens:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["task"], "\n";
if ($reply["task"] === "rewrite") { foreach ($reply["changes"] as $x) echo $x["id"], " ", $x["change"], "\n"; }
else { foreach ($reply["findings"] as $x) echo $x["id"], " ", $x["fix"], "\n"; }
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"form-lens:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>())!;
var task = (string?)reply["task"];
Console.WriteLine($"{j["charged_credits"]} {task}");
if (task == "rewrite") { foreach (var x in reply["changes"]!.AsArray()) Console.WriteLine($"{x!["id"]} {x["change"]}"); }
else { foreach (var x in reply["findings"]!.AsArray()) Console.WriteLine($"{x!["id"]} {x["fix"]}"); }
Step 6 — POST /run-stream
The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.
# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: form-lens:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json
rev = json.load(open("input.json")) # the same run input as input.json: {"task": "review", "form": "Form: Sign in
Targets: Android and iOS
Form widget: SignInForm", "code": "import 'package:flutter/material.dart';
...", "carry": "", "facts": "The page's scan of 40 lines ...
Findings (id | area | severity | line | where | evidence | source):
- F1 | architecture | blocker | line 8 | ..."}
# - see the input table
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "form-lens:" + rev["task"] + ":" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `form-lens:${rev.task}:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "form-lens:"+input["task"].(string)+":"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "form-lens:" + input.get("task") + ":" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "form-lens:#{JSON.parse(rev)["task"]}:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: form-lens:" . json_decode($rev, true)["task"] . ":" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"form-lens:{input["task"]}:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);