@lotics/cli 0.198.0 → 0.205.0
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.
- package/AGENTS.md +24 -0
- package/README.md +19 -3
- package/dist/probe_page.js +59 -7
- package/dist/src/cli.js +3295 -503
- package/dist/src/client.d.ts +117 -3
- package/dist/src/client.js +80 -13
- package/docs/building_an_app.md +23 -9
- package/docs/cli_reference.md +19 -17
- package/package.json +1 -1
package/dist/src/client.d.ts
CHANGED
|
@@ -92,10 +92,27 @@ export interface ReportFrame {
|
|
|
92
92
|
tried?: string;
|
|
93
93
|
wanted?: string;
|
|
94
94
|
}
|
|
95
|
+
/** One promise a refused write would break, as the tool transport states it. */
|
|
96
|
+
export interface ToolBreakingChange {
|
|
97
|
+
kind: string;
|
|
98
|
+
alias: string;
|
|
99
|
+
path: string;
|
|
100
|
+
rule?: string;
|
|
101
|
+
message?: string;
|
|
102
|
+
app_id?: string;
|
|
103
|
+
}
|
|
95
104
|
export interface ToolExecuteResult {
|
|
96
105
|
result: unknown;
|
|
97
106
|
model_output?: string;
|
|
98
107
|
error?: string;
|
|
108
|
+
/** The refusal's `code` — the same discriminator the HTTP envelope carries,
|
|
109
|
+
* and the only part of a failure a caller may branch on. Absent when the
|
|
110
|
+
* failure was not a typed error, and from any server older than it. */
|
|
111
|
+
error_code?: string;
|
|
112
|
+
/** A refused input addressed per field: one sentence from a typed alias, a
|
|
113
|
+
* list from a rejecting table workflow. */
|
|
114
|
+
field_errors?: Record<string, string> | Record<string, string[]>;
|
|
115
|
+
breaking_changes?: readonly ToolBreakingChange[];
|
|
99
116
|
}
|
|
100
117
|
/**
|
|
101
118
|
* A settled (or in-flight) app-agent run, the transcript-excluded projection
|
|
@@ -352,6 +369,14 @@ export interface ScaffoldWorkspaceResult {
|
|
|
352
369
|
table_id: string;
|
|
353
370
|
/** False when a table of that label already existed and was adopted. */
|
|
354
371
|
created: boolean;
|
|
372
|
+
/**
|
|
373
|
+
* What this run put ON that table — the delta a schema write is confirmed by.
|
|
374
|
+
* Absent from an instance too old to state it, so the line omits the counts
|
|
375
|
+
* rather than reporting a run that added nothing.
|
|
376
|
+
*/
|
|
377
|
+
created_fields?: number;
|
|
378
|
+
created_options?: number;
|
|
379
|
+
created_views?: number;
|
|
355
380
|
}>;
|
|
356
381
|
/**
|
|
357
382
|
* Every role the model declares. `created` is false when a GROUP of that name
|
|
@@ -396,6 +421,12 @@ export interface WorkspaceModelExport {
|
|
|
396
421
|
area: string;
|
|
397
422
|
message: string;
|
|
398
423
|
}>;
|
|
424
|
+
/**
|
|
425
|
+
* Labels of the entities the link closure added — the tables the caller did not
|
|
426
|
+
* name and a link pulled in. Absent from an instance too old to state it, which
|
|
427
|
+
* is why the printout says nothing rather than "0 pulled in".
|
|
428
|
+
*/
|
|
429
|
+
pulled_in?: string[];
|
|
399
430
|
}
|
|
400
431
|
/**
|
|
401
432
|
* A request the API refused. The message carries the status and the server's
|
|
@@ -618,12 +649,28 @@ export declare class LoticsClient {
|
|
|
618
649
|
*/
|
|
619
650
|
private buildHeaders;
|
|
620
651
|
private request;
|
|
652
|
+
/** `email` is null for a credential no person signs in as; `account_type` is absent from an older server. */
|
|
653
|
+
/** `credential_origin` describes the CREDENTIAL this call carried, not the
|
|
654
|
+
* member above: `sign_in` for a terminal somebody signed in from, `api_key`
|
|
655
|
+
* for one an admin issued. Null for a row minted before it was recorded, and
|
|
656
|
+
* absent from an older server — both mean not told. */
|
|
621
657
|
whoami(): Promise<{
|
|
622
658
|
member_id: string;
|
|
623
|
-
email: string;
|
|
659
|
+
email: string | null;
|
|
624
660
|
name: string;
|
|
661
|
+
account_type?: string;
|
|
625
662
|
organization_id: string;
|
|
626
663
|
organization_name: string;
|
|
664
|
+
credential_origin?: "api_key" | "sign_in" | null;
|
|
665
|
+
}>;
|
|
666
|
+
/**
|
|
667
|
+
* Hand this client's own credential back — what `lotics auth logout` calls
|
|
668
|
+
* before it forgets the key, so signing out ends the credential instead of
|
|
669
|
+
* leaving a live one on a server nobody can see it from. Takes no id: the
|
|
670
|
+
* only thing it can revoke is the key these requests carry.
|
|
671
|
+
*/
|
|
672
|
+
revokeSelfCredential(): Promise<{
|
|
673
|
+
id: string;
|
|
627
674
|
}>;
|
|
628
675
|
/**
|
|
629
676
|
* File a hand-written report, or a list of them in one call. Unlike a
|
|
@@ -639,6 +686,7 @@ export declare class LoticsClient {
|
|
|
639
686
|
app_id?: string;
|
|
640
687
|
}): Promise<{
|
|
641
688
|
accepted: number;
|
|
689
|
+
report_ids?: string[];
|
|
642
690
|
}>;
|
|
643
691
|
setWorkspaceId(id: string): void;
|
|
644
692
|
/** The workspace id the client targets (the `x-workspace-id` header), if resolved. */
|
|
@@ -937,7 +985,12 @@ export declare class LoticsClient {
|
|
|
937
985
|
* provenance is a 400, and one already on the latest release a 409 — both
|
|
938
986
|
* refusals, both before any write. Admin-only.
|
|
939
987
|
*/
|
|
940
|
-
upgradeApp(app_id: string
|
|
988
|
+
upgradeApp(app_id: string,
|
|
989
|
+
/** Apply the new release even though it breaks what this app's published API
|
|
990
|
+
* promises, snapshotting the broken contract as a new version. */
|
|
991
|
+
opts?: {
|
|
992
|
+
acknowledge_breaking_api_change?: boolean;
|
|
993
|
+
}): Promise<AppUpgradeResult>;
|
|
941
994
|
/**
|
|
942
995
|
* Capture live records from this workspace as a starter's sample data.
|
|
943
996
|
*
|
|
@@ -1063,6 +1116,20 @@ export declare class LoticsClient {
|
|
|
1063
1116
|
name: string;
|
|
1064
1117
|
private_filters: unknown;
|
|
1065
1118
|
}>>;
|
|
1119
|
+
/**
|
|
1120
|
+
* The organization's member groups — the directory `lotics app codegen` turns
|
|
1121
|
+
* into the `GRP` alias map.
|
|
1122
|
+
*
|
|
1123
|
+
* Over the HTTP route rather than `query_member_groups`, because the tool is
|
|
1124
|
+
* admin-only and the route is member-visible by design (`docs/iam.md` § Groups:
|
|
1125
|
+
* reading the directory is not administration). Codegen runs for every author,
|
|
1126
|
+
* so an admin-only read here would leave a non-admin's `GRP` empty and their
|
|
1127
|
+
* app unbuildable.
|
|
1128
|
+
*/
|
|
1129
|
+
getMemberGroups(): Promise<Array<{
|
|
1130
|
+
id: string;
|
|
1131
|
+
name: string;
|
|
1132
|
+
}>>;
|
|
1066
1133
|
/**
|
|
1067
1134
|
* Resolve the display name + fields (incl. select options) of the given tables
|
|
1068
1135
|
* — the schema `lotics app codegen` turns into the runtime `.lotics/app_fields.ts`
|
|
@@ -1094,6 +1161,40 @@ export declare class LoticsClient {
|
|
|
1094
1161
|
public_subdomain: string;
|
|
1095
1162
|
origin: string;
|
|
1096
1163
|
}>;
|
|
1164
|
+
/**
|
|
1165
|
+
* Publish the app's API — the owner's promise about what its declared queries
|
|
1166
|
+
* and workflows return to a caller outside the app.
|
|
1167
|
+
*
|
|
1168
|
+
* Snapshots the contract and answers the version it took, plus what the
|
|
1169
|
+
* published surface exposes that the owner may not have intended. A query
|
|
1170
|
+
* that does not name its columns is refused (400) before anything is written:
|
|
1171
|
+
* the app cannot promise field names it never stated.
|
|
1172
|
+
*/
|
|
1173
|
+
publishAppApi(app_id: string): Promise<{
|
|
1174
|
+
app_id: string;
|
|
1175
|
+
contract_version: number;
|
|
1176
|
+
published_at: string;
|
|
1177
|
+
warnings: string[];
|
|
1178
|
+
}>;
|
|
1179
|
+
/** End the promise. `unpublished: false` means the app was publishing nothing,
|
|
1180
|
+
* which this leaves unchanged. The superseded snapshot stays, so a later
|
|
1181
|
+
* publish continues the numbering rather than reusing a version. */
|
|
1182
|
+
unpublishAppApi(app_id: string): Promise<{
|
|
1183
|
+
app_id: string;
|
|
1184
|
+
unpublished: boolean;
|
|
1185
|
+
}>;
|
|
1186
|
+
/** Whether the app is publishing an API, and which contract version. */
|
|
1187
|
+
getAppApiPublication(app_id: string): Promise<{
|
|
1188
|
+
published: boolean;
|
|
1189
|
+
contract_version: number | null;
|
|
1190
|
+
published_at: string | null;
|
|
1191
|
+
}>;
|
|
1192
|
+
/**
|
|
1193
|
+
* The published API as an OpenAPI 3.1 document — what a consumer's own
|
|
1194
|
+
* generator reads. Rendered from the live SNAPSHOT rather than the manifest,
|
|
1195
|
+
* so it describes what the app has promised; 404 while nothing is published.
|
|
1196
|
+
*/
|
|
1197
|
+
getAppOpenApiDocument(app_id: string): Promise<Record<string, unknown>>;
|
|
1097
1198
|
getAppVersion(app_id: string, version_id: string): Promise<{
|
|
1098
1199
|
id: string;
|
|
1099
1200
|
app_id: string;
|
|
@@ -1173,6 +1274,9 @@ export declare class LoticsClient {
|
|
|
1173
1274
|
* server refuses it when the live body has moved since, rather than
|
|
1174
1275
|
* letting a stale copy overwrite an edit its author never saw. */
|
|
1175
1276
|
expected_body_sha?: string;
|
|
1277
|
+
/** Carry out this write even though it breaks what the app's published API
|
|
1278
|
+
* promises, snapshotting the broken contract as a new version. */
|
|
1279
|
+
acknowledge_breaking_api_change?: boolean;
|
|
1176
1280
|
}): Promise<ToolExecuteResult>;
|
|
1177
1281
|
/**
|
|
1178
1282
|
* Bind (create or replace) an app query by alias via the `set_app_query` tool
|
|
@@ -1188,7 +1292,10 @@ export declare class LoticsClient {
|
|
|
1188
1292
|
description?: string;
|
|
1189
1293
|
},
|
|
1190
1294
|
/** The fingerprint this edit was based on — makes the write conditional. */
|
|
1191
|
-
expected_sha?: string
|
|
1295
|
+
expected_sha?: string,
|
|
1296
|
+
/** Carry out this write even though it breaks what the app's published API
|
|
1297
|
+
* promises, snapshotting the broken contract as a new version. */
|
|
1298
|
+
acknowledge_breaking_api_change?: boolean): Promise<ToolExecuteResult>;
|
|
1192
1299
|
/**
|
|
1193
1300
|
* Bind (create or replace) an app agent by alias via the `set_app_agent` tool
|
|
1194
1301
|
* — the deploy-free authoring path for `apps.agents`, parallel to
|
|
@@ -1366,6 +1473,13 @@ export declare class LoticsClient {
|
|
|
1366
1473
|
workflow_aliases?: string[];
|
|
1367
1474
|
agent_aliases?: string[];
|
|
1368
1475
|
query_aliases?: string[];
|
|
1476
|
+
/**
|
|
1477
|
+
* Ship this version even though its manifest breaks what the app's published
|
|
1478
|
+
* API promises, snapshotting the broken contract as a new version. A
|
|
1479
|
+
* multipart field is text, so it goes over as the two spellings the server's
|
|
1480
|
+
* own schema admits.
|
|
1481
|
+
*/
|
|
1482
|
+
acknowledge_breaking_api_change?: boolean;
|
|
1369
1483
|
}): Promise<{
|
|
1370
1484
|
version_id: string;
|
|
1371
1485
|
version_number: number;
|
package/dist/src/client.js
CHANGED
|
@@ -266,10 +266,12 @@ var LoticsClient = class {
|
|
|
266
266
|
body = {};
|
|
267
267
|
}
|
|
268
268
|
const message = typeof body.message === "string" ? body.message : text;
|
|
269
|
+
const hint = typeof body.hint === "string" ? `
|
|
270
|
+
${body.hint}` : "";
|
|
269
271
|
const trace = requestId === void 0 ? "" : `
|
|
270
272
|
|
|
271
273
|
Request id: ${requestId} \u2014 quote this to Lotics support.`;
|
|
272
|
-
throw new LoticsRequestError(`${response.status}: ${message}${trace}`, response.status, body);
|
|
274
|
+
throw new LoticsRequestError(`${response.status}: ${message}${hint}${trace}`, response.status, body);
|
|
273
275
|
}
|
|
274
276
|
/**
|
|
275
277
|
* The backend's `log()` middleware registers `user-agent`,
|
|
@@ -319,9 +321,23 @@ var LoticsClient = class {
|
|
|
319
321
|
if (!response.ok) await this.throwResponseError(response, headers["x-request-id"]);
|
|
320
322
|
return response.json();
|
|
321
323
|
}
|
|
324
|
+
/** `email` is null for a credential no person signs in as; `account_type` is absent from an older server. */
|
|
325
|
+
/** `credential_origin` describes the CREDENTIAL this call carried, not the
|
|
326
|
+
* member above: `sign_in` for a terminal somebody signed in from, `api_key`
|
|
327
|
+
* for one an admin issued. Null for a row minted before it was recorded, and
|
|
328
|
+
* absent from an older server — both mean not told. */
|
|
322
329
|
async whoami() {
|
|
323
330
|
return this.request("GET", "/v1/cli/whoami");
|
|
324
331
|
}
|
|
332
|
+
/**
|
|
333
|
+
* Hand this client's own credential back — what `lotics auth logout` calls
|
|
334
|
+
* before it forgets the key, so signing out ends the credential instead of
|
|
335
|
+
* leaving a live one on a server nobody can see it from. Takes no id: the
|
|
336
|
+
* only thing it can revoke is the key these requests carry.
|
|
337
|
+
*/
|
|
338
|
+
async revokeSelfCredential() {
|
|
339
|
+
return this.request("DELETE", "/v1/api_keys/self");
|
|
340
|
+
}
|
|
325
341
|
/**
|
|
326
342
|
* File a hand-written report, or a list of them in one call. Unlike a
|
|
327
343
|
* telemetry flush, the headers this stamps are CORRECT: the invocation making
|
|
@@ -517,8 +533,12 @@ var LoticsClient = class {
|
|
|
517
533
|
* provenance is a 400, and one already on the latest release a 409 — both
|
|
518
534
|
* refusals, both before any write. Admin-only.
|
|
519
535
|
*/
|
|
520
|
-
async upgradeApp(app_id) {
|
|
521
|
-
return this.request(
|
|
536
|
+
async upgradeApp(app_id, opts = {}) {
|
|
537
|
+
return this.request(
|
|
538
|
+
"POST",
|
|
539
|
+
`/v1/apps/${encodeURIComponent(app_id)}/upgrade`,
|
|
540
|
+
opts.acknowledge_breaking_api_change ? { acknowledge_breaking_api_change: true } : {}
|
|
541
|
+
);
|
|
522
542
|
}
|
|
523
543
|
/**
|
|
524
544
|
* Capture live records from this workspace as a starter's sample data.
|
|
@@ -650,6 +670,23 @@ var LoticsClient = class {
|
|
|
650
670
|
);
|
|
651
671
|
return tables.filter((table) => table !== null);
|
|
652
672
|
}
|
|
673
|
+
/**
|
|
674
|
+
* The organization's member groups — the directory `lotics app codegen` turns
|
|
675
|
+
* into the `GRP` alias map.
|
|
676
|
+
*
|
|
677
|
+
* Over the HTTP route rather than `query_member_groups`, because the tool is
|
|
678
|
+
* admin-only and the route is member-visible by design (`docs/iam.md` § Groups:
|
|
679
|
+
* reading the directory is not administration). Codegen runs for every author,
|
|
680
|
+
* so an admin-only read here would leave a non-admin's `GRP` empty and their
|
|
681
|
+
* app unbuildable.
|
|
682
|
+
*/
|
|
683
|
+
async getMemberGroups() {
|
|
684
|
+
const { groups } = await this.request(
|
|
685
|
+
"GET",
|
|
686
|
+
"/v1/iam/member_groups"
|
|
687
|
+
);
|
|
688
|
+
return groups.map((group) => ({ id: group.id, name: group.name }));
|
|
689
|
+
}
|
|
653
690
|
/**
|
|
654
691
|
* Resolve the display name + fields (incl. select options) of the given tables
|
|
655
692
|
* — the schema `lotics app codegen` turns into the runtime `.lotics/app_fields.ts`
|
|
@@ -695,6 +732,36 @@ var LoticsClient = class {
|
|
|
695
732
|
{ public_subdomain }
|
|
696
733
|
);
|
|
697
734
|
}
|
|
735
|
+
/**
|
|
736
|
+
* Publish the app's API — the owner's promise about what its declared queries
|
|
737
|
+
* and workflows return to a caller outside the app.
|
|
738
|
+
*
|
|
739
|
+
* Snapshots the contract and answers the version it took, plus what the
|
|
740
|
+
* published surface exposes that the owner may not have intended. A query
|
|
741
|
+
* that does not name its columns is refused (400) before anything is written:
|
|
742
|
+
* the app cannot promise field names it never stated.
|
|
743
|
+
*/
|
|
744
|
+
async publishAppApi(app_id) {
|
|
745
|
+
return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/api/publish`, {});
|
|
746
|
+
}
|
|
747
|
+
/** End the promise. `unpublished: false` means the app was publishing nothing,
|
|
748
|
+
* which this leaves unchanged. The superseded snapshot stays, so a later
|
|
749
|
+
* publish continues the numbering rather than reusing a version. */
|
|
750
|
+
async unpublishAppApi(app_id) {
|
|
751
|
+
return this.request("DELETE", `/v1/apps/${encodeURIComponent(app_id)}/api/publish`);
|
|
752
|
+
}
|
|
753
|
+
/** Whether the app is publishing an API, and which contract version. */
|
|
754
|
+
async getAppApiPublication(app_id) {
|
|
755
|
+
return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/api/publication`);
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* The published API as an OpenAPI 3.1 document — what a consumer's own
|
|
759
|
+
* generator reads. Rendered from the live SNAPSHOT rather than the manifest,
|
|
760
|
+
* so it describes what the app has promised; 404 while nothing is published.
|
|
761
|
+
*/
|
|
762
|
+
async getAppOpenApiDocument(app_id) {
|
|
763
|
+
return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/openapi.json`);
|
|
764
|
+
}
|
|
698
765
|
async getAppVersion(app_id, version_id) {
|
|
699
766
|
return this.request(
|
|
700
767
|
"GET",
|
|
@@ -784,7 +851,10 @@ var LoticsClient = class {
|
|
|
784
851
|
...body.outputs ? { outputs: body.outputs } : {},
|
|
785
852
|
...body.name ? { name: body.name } : {},
|
|
786
853
|
...body.description ? { description: body.description } : {},
|
|
787
|
-
...body.expected_body_sha ? { expected_body_sha: body.expected_body_sha } : {}
|
|
854
|
+
...body.expected_body_sha ? { expected_body_sha: body.expected_body_sha } : {},
|
|
855
|
+
// Sent only when the caller asked for it: absent means "refuse a break",
|
|
856
|
+
// which is the answer a caller who said nothing gave.
|
|
857
|
+
...body.acknowledge_breaking_api_change ? { acknowledge_breaking_api_change: true } : {}
|
|
788
858
|
});
|
|
789
859
|
}
|
|
790
860
|
/**
|
|
@@ -795,12 +865,13 @@ var LoticsClient = class {
|
|
|
795
865
|
* deploy validates the manifest. Note: `apps.queries` is manifest-authoritative,
|
|
796
866
|
* so the next `lotics app deploy` overwrites this from the manifest.
|
|
797
867
|
*/
|
|
798
|
-
async setAppQuery(app_id, alias, declaration, expected_sha) {
|
|
868
|
+
async setAppQuery(app_id, alias, declaration, expected_sha, acknowledge_breaking_api_change) {
|
|
799
869
|
return this.execute("set_app_query", {
|
|
800
870
|
app_id,
|
|
801
871
|
alias,
|
|
802
872
|
declaration,
|
|
803
|
-
...expected_sha ? { expected_sha } : {}
|
|
873
|
+
...expected_sha ? { expected_sha } : {},
|
|
874
|
+
...acknowledge_breaking_api_change ? { acknowledge_breaking_api_change: true } : {}
|
|
804
875
|
});
|
|
805
876
|
}
|
|
806
877
|
/**
|
|
@@ -1003,16 +1074,12 @@ var LoticsClient = class {
|
|
|
1003
1074
|
formData.append("workflow_aliases", JSON.stringify(args.workflow_aliases ?? []));
|
|
1004
1075
|
formData.append("agent_aliases", JSON.stringify(args.agent_aliases ?? []));
|
|
1005
1076
|
formData.append("query_aliases", JSON.stringify(args.query_aliases ?? []));
|
|
1077
|
+
if (args.acknowledge_breaking_api_change) {
|
|
1078
|
+
formData.append("acknowledge_breaking_api_change", "true");
|
|
1079
|
+
}
|
|
1006
1080
|
const url = `${this.baseUrl}/v1/apps/${encodeURIComponent(args.app_id)}/versions`;
|
|
1007
1081
|
const headers = this.buildHeaders();
|
|
1008
1082
|
const response = await fetch(url, { method: "POST", headers, body: formData });
|
|
1009
|
-
if (response.status === 409) {
|
|
1010
|
-
const conflict = await response.json();
|
|
1011
|
-
const err = new Error(conflict.message ?? "Deploy conflict \u2014 pull required");
|
|
1012
|
-
err.code = "VERSION_CONFLICT";
|
|
1013
|
-
err.current_version_id = conflict.current_version_id ?? null;
|
|
1014
|
-
throw err;
|
|
1015
|
-
}
|
|
1016
1083
|
if (!response.ok) {
|
|
1017
1084
|
await this.throwResponseError(response, headers["x-request-id"]);
|
|
1018
1085
|
}
|
package/docs/building_an_app.md
CHANGED
|
@@ -99,13 +99,16 @@ lotics app codegen # no deploy, no version bump
|
|
|
99
99
|
```
|
|
100
100
|
|
|
101
101
|
Regenerates `.lotics/`: the three `.d.ts` companions that type `useQuery` / `useWorkflow` /
|
|
102
|
-
`useAgentRun`, and — when credentials resolve — `app_fields.ts`, exporting
|
|
103
|
-
`"fld_…"`)
|
|
102
|
+
`useAgentRun`, and — when credentials resolve — `app_fields.ts`, exporting four maps keyed by
|
|
103
|
+
display-name aliases: `F` (table → field → `"fld_…"`), `OPT` (table → select field → option →
|
|
104
|
+
`"opt_…"`), `TBL` (table → `"tbl_…"`) and `GRP` (member group → `"grp_…"`).
|
|
104
105
|
|
|
105
|
-
Address
|
|
106
|
+
Address every id by alias, never by a pasted one — a table id and a member group included:
|
|
106
107
|
|
|
107
108
|
```tsx
|
|
108
109
|
row.opt(r[F.SHIPMENT.direction]) === OPT.SHIPMENT.direction.export
|
|
110
|
+
useCommentCounts({ table_id: TBL.SHIPMENT });
|
|
111
|
+
useMembers({ group: GRP.sale });
|
|
109
112
|
```
|
|
110
113
|
|
|
111
114
|
An alias is slugified from the display name, so a rename on the platform MOVES it: re-run `codegen`
|
|
@@ -119,6 +122,11 @@ Author them as `kind: "project"` with a `filter`. A bare `from_table` over-expos
|
|
|
119
122
|
degrades at scale. Scope per-user reads with `is_current_member` **inside the template** — a
|
|
120
123
|
`member_id` passed from the client is an IDOR, since the caller chooses it.
|
|
121
124
|
|
|
125
|
+
Name each projected column — `{ "source": "fld_…", "output": "total" }` — and the row is then read
|
|
126
|
+
as `r.total`, with no field map on the read path; `lotics app create --from` emits exactly that.
|
|
127
|
+
Write one `description` per alias too: it is the line a chat or MCP caller chooses between them by,
|
|
128
|
+
and `lotics app check` exits 1 naming any alias that has none.
|
|
129
|
+
|
|
122
130
|
Decode cells with the `row.*` helpers (`row.text`, `row.opt`, `readSelect`, `readLinks`), never by
|
|
123
131
|
reaching into the raw shape: a select cell is `[{key,label}]`, and a hand-rolled reader silently
|
|
124
132
|
returns the wrong half.
|
|
@@ -169,7 +177,9 @@ writes the schema back into the manifest and refreshes that alias's types in pla
|
|
|
169
177
|
The alias's `description` rides along from the manifest. It is the one line an agent reads when
|
|
170
178
|
choosing between your workflows, so write it rather than leaving the generated placeholder — and
|
|
171
179
|
keep it inside 300 characters, the cap a push holds both a workflow's and a query's `description`
|
|
172
|
-
to, because both ride the capability catalog into the agent's prompt on every run.
|
|
180
|
+
to, because both ride the capability catalog into the agent's prompt on every run. `app check`
|
|
181
|
+
names every alias over it and exits 1, and `workflow set` / `query set` refuse before sending
|
|
182
|
+
anything, so a long line costs one edit rather than a failed push per alias.
|
|
173
183
|
|
|
174
184
|
**Every workflow you declare is also the chat agent's write surface.** So the alias's *shape* is an
|
|
175
185
|
agent-facing decision, not only a screen-facing one — take a list where one job covers many
|
|
@@ -286,17 +296,21 @@ actually carries, not what the source says it should.
|
|
|
286
296
|
|
|
287
297
|
```
|
|
288
298
|
npm run typecheck && npm run lint && npm test
|
|
289
|
-
lotics app check --screens
|
|
299
|
+
lotics app check --screens --shots shots/
|
|
300
|
+
# every pre-flight a deploy runs, without building or shipping,
|
|
290
301
|
# plus the portability gate a library publish applies, plus
|
|
291
|
-
# every screen rendered at 1280 and 375 and
|
|
302
|
+
# every screen rendered at 1280 and 375, measured, and written
|
|
303
|
+
# to shots/ as a PNG per screen and per record it opened
|
|
292
304
|
lotics app deploy -m "<what changed + why>"
|
|
293
305
|
```
|
|
294
306
|
|
|
295
307
|
**A green suite says nothing about how the screen LOOKS**, and that half starts with
|
|
296
308
|
`--screens`: it renders the app over its real data (Chrome needed), walks every tab at both
|
|
297
309
|
widths, and refuses what a review would; what it measures is in `lotics docs cli_reference`. What
|
|
298
|
-
it cannot measure is in `lotics docs reviewing` —
|
|
299
|
-
|
|
310
|
+
it cannot measure is in `lotics docs reviewing` — which is why `--shots <dir>` is on the same
|
|
311
|
+
line: it photographs the frame each probe read, so LOOKING at the app is the same run rather than
|
|
312
|
+
a dev server and a browser pass per screen. Open the 375 shots first. Both before the deploy, not
|
|
313
|
+
as an audit someone schedules after a complaint.
|
|
300
314
|
|
|
301
315
|
Every check above reads the SOURCE; none of them renders it. So the entire class of defect that
|
|
302
316
|
lives in the pixels — wrong form for the subject, a treatment that contradicts what an element
|
|
@@ -336,7 +350,7 @@ lotics app codegen # after any schema change
|
|
|
336
350
|
npm run typecheck # honest, because codegen is current
|
|
337
351
|
lotics run run_app_workflow '{"app_id":…,"alias":…}' # prove the mutation path
|
|
338
352
|
lotics app dev # prove the screen
|
|
339
|
-
lotics app check --screens
|
|
353
|
+
lotics app check --screens --shots shots/ # measure it AND photograph it, before anyone looks
|
|
340
354
|
…
|
|
341
355
|
lotics app workflow set <alias> # push the body; the server verifies
|
|
342
356
|
lotics app deploy -m "…" # pushes pending bindings, then ships
|