@askrjs/cli 0.0.12 → 0.0.13
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/dist/analyze.js +1 -1
- package/dist/cli.js +1 -1
- package/dist/{guardrails-GPM9PJqO.js → guardrails-D6KCKS-H.js} +1 -1
- package/dist/{runner-BRjjPhZY.js → runner-BSs9Ow3t.js} +504 -6
- package/dist/{runner-Ca43qqzG.js → runner-DetGSvGf.js} +1 -1
- package/dist/templates/spa/src/pages/app/admin-home.tsx +136 -133
- package/dist/templates/startkit/src/components/data-table.tsx +33 -33
- package/package.json +10 -2
package/dist/analyze.js
CHANGED
|
@@ -71,7 +71,7 @@ async function runAnalyzeCli(args = process.argv.slice(2), io = console, runtime
|
|
|
71
71
|
io.log(helpText.trimEnd());
|
|
72
72
|
return 0;
|
|
73
73
|
}
|
|
74
|
-
const report = await (runtime.analyze ?? (await import("./runner-
|
|
74
|
+
const report = await (runtime.analyze ?? (await import("./runner-BSs9Ow3t.js")).runAnalysis)({
|
|
75
75
|
cwd: parsed.cwd,
|
|
76
76
|
workspacePatterns: parsed.workspacePatterns,
|
|
77
77
|
check: parsed.check
|
package/dist/cli.js
CHANGED
|
@@ -90,7 +90,7 @@ async function runCli(args = process.argv.slice(2), io = console) {
|
|
|
90
90
|
return runAnalyzeCli(args.slice(1), io);
|
|
91
91
|
}
|
|
92
92
|
if (command === "check" || command === "doctor" || command === "repair") {
|
|
93
|
-
const { runGuardrailCli } = await import("./guardrails-
|
|
93
|
+
const { runGuardrailCli } = await import("./guardrails-D6KCKS-H.js");
|
|
94
94
|
return runGuardrailCli(command, args.slice(1), io);
|
|
95
95
|
}
|
|
96
96
|
if (command === "generate") {
|
|
@@ -98,7 +98,7 @@ async function runGuardrailCli(command, args = process.argv.slice(2), io = conso
|
|
|
98
98
|
cwd: parsed.cwd,
|
|
99
99
|
workspacePatterns: parsed.workspacePatterns
|
|
100
100
|
};
|
|
101
|
-
const { runCheck, runDoctor, runRepair } = await import("./runner-
|
|
101
|
+
const { runCheck, runDoctor, runRepair } = await import("./runner-DetGSvGf.js");
|
|
102
102
|
const report = command === "doctor" ? await runDoctor(options, runtime) : command === "repair" ? await runRepair(options) : await runCheck(options, runtime);
|
|
103
103
|
if (parsed.json) io.log(JSON.stringify(report));
|
|
104
104
|
else if (report.command === "doctor") printDoctor(report, io);
|
|
@@ -11,6 +11,7 @@ const DEFAULT_ANALYZE_EXCLUDES = [
|
|
|
11
11
|
"**/build/**",
|
|
12
12
|
"**/coverage/**",
|
|
13
13
|
"**/.git/**",
|
|
14
|
+
"**/.askr/**",
|
|
14
15
|
"**/.next/**",
|
|
15
16
|
"**/.output/**",
|
|
16
17
|
"**/.turbo/**",
|
|
@@ -237,6 +238,273 @@ const RENDER_SCOPED_CONCEPTS = /* @__PURE__ */ new Set([
|
|
|
237
238
|
const POSITIONAL_DATA_CONCEPTS = /* @__PURE__ */ new Set(["createQuery", "createMutation"]);
|
|
238
239
|
const ASKR_MODULE_PATTERN = /^@askrjs\/askr(?:\/|$)/;
|
|
239
240
|
//#endregion
|
|
241
|
+
//#region src/analyze/call-graph.ts
|
|
242
|
+
const CALL_GRAPH_CACHE = /* @__PURE__ */ new WeakMap();
|
|
243
|
+
function isLocalFunction(node) {
|
|
244
|
+
return Boolean(node && ts.isFunctionLike(node) && "body" in node && node.body);
|
|
245
|
+
}
|
|
246
|
+
function containingLocalFunction(node) {
|
|
247
|
+
for (let current = node.parent; current; current = current.parent) if (isLocalFunction(current)) return current;
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
function declarationFunction(declaration) {
|
|
251
|
+
if (!declaration) return null;
|
|
252
|
+
if (isLocalFunction(declaration)) return declaration;
|
|
253
|
+
if (ts.isVariableDeclaration(declaration) && declaration.initializer && isLocalFunction(declaration.initializer)) return declaration.initializer;
|
|
254
|
+
if (ts.isPropertyAssignment(declaration) && isLocalFunction(declaration.initializer)) return declaration.initializer;
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
function symbolForExpression(checker, expression) {
|
|
258
|
+
const location = ts.isPropertyAccessExpression(expression) ? expression.name : expression;
|
|
259
|
+
let symbol = checker.getSymbolAtLocation(location) ?? checker.getSymbolAtLocation(expression);
|
|
260
|
+
if (symbol && (symbol.flags & ts.SymbolFlags.Alias) !== 0) symbol = checker.getAliasedSymbol(symbol);
|
|
261
|
+
return symbol;
|
|
262
|
+
}
|
|
263
|
+
function resolveLocalFunction(checker, sourceFiles, expression) {
|
|
264
|
+
const symbol = symbolForExpression(checker, expression);
|
|
265
|
+
const declarations = [symbol?.valueDeclaration, ...symbol?.declarations ?? []];
|
|
266
|
+
for (const declaration of declarations) {
|
|
267
|
+
const fn = declarationFunction(declaration);
|
|
268
|
+
if (fn && sourceFiles.has(fn.getSourceFile())) return fn;
|
|
269
|
+
}
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
function localNamedFunction(sourceFile, name) {
|
|
273
|
+
for (const statement of sourceFile.statements) {
|
|
274
|
+
if (ts.isFunctionDeclaration(statement) && statement.name?.text === name) return isLocalFunction(statement) ? statement : null;
|
|
275
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
276
|
+
for (const declaration of statement.declarationList.declarations) if (ts.isIdentifier(declaration.name) && declaration.name.text === name && declaration.initializer && isLocalFunction(declaration.initializer)) return declaration.initializer;
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
function localCallGraph(context) {
|
|
281
|
+
const cached = CALL_GRAPH_CACHE.get(context.program);
|
|
282
|
+
if (cached) return cached;
|
|
283
|
+
const sourceFiles = new Set(context.sourceFiles);
|
|
284
|
+
const sourceByPath = new Map(context.sourceFiles.map((sourceFile) => [path.resolve(sourceFile.fileName), sourceFile]));
|
|
285
|
+
const moduleCache = /* @__PURE__ */ new Map();
|
|
286
|
+
const resolveModule = (containingFile, specifier) => {
|
|
287
|
+
if (!specifier.startsWith(".")) return null;
|
|
288
|
+
const cacheKey = `${containingFile.fileName}\0${specifier}`;
|
|
289
|
+
if (moduleCache.has(cacheKey)) return moduleCache.get(cacheKey) ?? null;
|
|
290
|
+
const base = path.resolve(path.dirname(containingFile.fileName), specifier);
|
|
291
|
+
const withoutJsExtension = base.replace(/\.(?:c|m)?js$/, "");
|
|
292
|
+
const candidates = [
|
|
293
|
+
base,
|
|
294
|
+
`${base}.ts`,
|
|
295
|
+
`${base}.tsx`,
|
|
296
|
+
`${base}.mts`,
|
|
297
|
+
`${base}.cts`,
|
|
298
|
+
`${base}.js`,
|
|
299
|
+
`${base}.jsx`,
|
|
300
|
+
`${withoutJsExtension}.ts`,
|
|
301
|
+
`${withoutJsExtension}.tsx`,
|
|
302
|
+
path.join(base, "index.ts"),
|
|
303
|
+
path.join(base, "index.tsx"),
|
|
304
|
+
path.join(base, "index.js")
|
|
305
|
+
];
|
|
306
|
+
for (const candidate of candidates) {
|
|
307
|
+
const resolved = sourceByPath.get(candidate);
|
|
308
|
+
if (resolved) {
|
|
309
|
+
moduleCache.set(cacheKey, resolved);
|
|
310
|
+
return resolved;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
moduleCache.set(cacheKey, null);
|
|
314
|
+
return null;
|
|
315
|
+
};
|
|
316
|
+
const exportCache = /* @__PURE__ */ new Map();
|
|
317
|
+
const exportedFunction = (sourceFile, name, seen = /* @__PURE__ */ new Set()) => {
|
|
318
|
+
const key = `${sourceFile.fileName}\0${name}`;
|
|
319
|
+
if (exportCache.has(key)) return exportCache.get(key) ?? null;
|
|
320
|
+
if (seen.has(key)) return null;
|
|
321
|
+
seen.add(key);
|
|
322
|
+
const direct = localNamedFunction(sourceFile, name);
|
|
323
|
+
if (direct) {
|
|
324
|
+
exportCache.set(key, direct);
|
|
325
|
+
return direct;
|
|
326
|
+
}
|
|
327
|
+
exportCache.set(key, null);
|
|
328
|
+
for (const statement of sourceFile.statements) {
|
|
329
|
+
if (name === "default" && ts.isFunctionDeclaration(statement) && statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword) && isLocalFunction(statement)) {
|
|
330
|
+
exportCache.set(key, statement);
|
|
331
|
+
return statement;
|
|
332
|
+
}
|
|
333
|
+
if (!ts.isExportDeclaration(statement)) continue;
|
|
334
|
+
const target = statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier) ? resolveModule(sourceFile, statement.moduleSpecifier.text) : sourceFile;
|
|
335
|
+
if (!target) continue;
|
|
336
|
+
if (!statement.exportClause) {
|
|
337
|
+
const resolved = exportedFunction(target, name, seen);
|
|
338
|
+
if (resolved) {
|
|
339
|
+
exportCache.set(key, resolved);
|
|
340
|
+
return resolved;
|
|
341
|
+
}
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (!ts.isNamedExports(statement.exportClause)) continue;
|
|
345
|
+
const element = statement.exportClause.elements.find((candidate) => candidate.name.text === name);
|
|
346
|
+
if (!element) continue;
|
|
347
|
+
const imported = element.propertyName?.text ?? element.name.text;
|
|
348
|
+
const resolved = target === sourceFile ? localNamedFunction(sourceFile, imported) : exportedFunction(target, imported, seen);
|
|
349
|
+
exportCache.set(key, resolved);
|
|
350
|
+
return resolved;
|
|
351
|
+
}
|
|
352
|
+
return null;
|
|
353
|
+
};
|
|
354
|
+
const importBindings = /* @__PURE__ */ new Map();
|
|
355
|
+
for (const sourceFile of context.sourceFiles) {
|
|
356
|
+
const named = /* @__PURE__ */ new Map();
|
|
357
|
+
const namespaces = /* @__PURE__ */ new Map();
|
|
358
|
+
for (const statement of sourceFile.statements) {
|
|
359
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
360
|
+
const target = resolveModule(sourceFile, statement.moduleSpecifier.text);
|
|
361
|
+
if (!target) continue;
|
|
362
|
+
const clause = statement.importClause;
|
|
363
|
+
if (clause?.name) named.set(clause.name.text, {
|
|
364
|
+
source: target,
|
|
365
|
+
imported: "default"
|
|
366
|
+
});
|
|
367
|
+
const bindings = clause?.namedBindings;
|
|
368
|
+
if (bindings && ts.isNamedImports(bindings)) for (const element of bindings.elements) named.set(element.name.text, {
|
|
369
|
+
source: target,
|
|
370
|
+
imported: element.propertyName?.text ?? element.name.text
|
|
371
|
+
});
|
|
372
|
+
else if (bindings && ts.isNamespaceImport(bindings)) namespaces.set(bindings.name.text, target);
|
|
373
|
+
}
|
|
374
|
+
importBindings.set(sourceFile, {
|
|
375
|
+
named,
|
|
376
|
+
namespaces
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
const resolveImportedFunction = (expression) => {
|
|
380
|
+
const imports = importBindings.get(expression.getSourceFile());
|
|
381
|
+
if (!imports) return null;
|
|
382
|
+
if (ts.isIdentifier(expression)) {
|
|
383
|
+
const imported = imports.named.get(expression.text);
|
|
384
|
+
return imported ? exportedFunction(imported.source, imported.imported) : null;
|
|
385
|
+
}
|
|
386
|
+
if (ts.isPropertyAccessExpression(expression) && ts.isIdentifier(expression.expression)) {
|
|
387
|
+
const source = imports.namespaces.get(expression.expression.text);
|
|
388
|
+
return source ? exportedFunction(source, expression.name.text) : null;
|
|
389
|
+
}
|
|
390
|
+
return null;
|
|
391
|
+
};
|
|
392
|
+
const targetByExpression = /* @__PURE__ */ new WeakMap();
|
|
393
|
+
const resolveFunctionExpression = (expression) => {
|
|
394
|
+
if (targetByExpression.has(expression)) return targetByExpression.get(expression) ?? null;
|
|
395
|
+
const target = resolveLocalFunction(context.checker, sourceFiles, expression) ?? resolveImportedFunction(expression);
|
|
396
|
+
targetByExpression.set(expression, target);
|
|
397
|
+
return target;
|
|
398
|
+
};
|
|
399
|
+
const functions = /* @__PURE__ */ new Set();
|
|
400
|
+
const callNodes = [];
|
|
401
|
+
const constructionNodes = [];
|
|
402
|
+
const walk = (node) => {
|
|
403
|
+
if (isLocalFunction(node)) functions.add(node);
|
|
404
|
+
if (ts.isCallExpression(node)) callNodes.push(node);
|
|
405
|
+
if (ts.isNewExpression(node)) constructionNodes.push(node);
|
|
406
|
+
ts.forEachChild(node, walk);
|
|
407
|
+
};
|
|
408
|
+
for (const sourceFile of context.sourceFiles) walk(sourceFile);
|
|
409
|
+
const targetByCall = /* @__PURE__ */ new Map();
|
|
410
|
+
const graph = {
|
|
411
|
+
calls: callNodes.map((node) => {
|
|
412
|
+
const target = resolveFunctionExpression(node.expression);
|
|
413
|
+
targetByCall.set(node, target);
|
|
414
|
+
return {
|
|
415
|
+
node,
|
|
416
|
+
owner: containingLocalFunction(node),
|
|
417
|
+
target
|
|
418
|
+
};
|
|
419
|
+
}),
|
|
420
|
+
constructions: constructionNodes.map((node) => ({
|
|
421
|
+
node,
|
|
422
|
+
owner: containingLocalFunction(node)
|
|
423
|
+
})),
|
|
424
|
+
functions,
|
|
425
|
+
functionForCall(call) {
|
|
426
|
+
return targetByCall.get(call) ?? null;
|
|
427
|
+
},
|
|
428
|
+
functionForExpression(expression) {
|
|
429
|
+
return resolveFunctionExpression(expression);
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
CALL_GRAPH_CACHE.set(context.program, graph);
|
|
433
|
+
return graph;
|
|
434
|
+
}
|
|
435
|
+
function addAll(target, source) {
|
|
436
|
+
let changed = false;
|
|
437
|
+
for (const value of source) {
|
|
438
|
+
if (target.has(value)) continue;
|
|
439
|
+
target.add(value);
|
|
440
|
+
changed = true;
|
|
441
|
+
}
|
|
442
|
+
return changed;
|
|
443
|
+
}
|
|
444
|
+
function summarizeTransitiveCalls(context, classifyDirectOperation, callIsUnstable) {
|
|
445
|
+
const graph = localCallGraph(context);
|
|
446
|
+
const mutable = /* @__PURE__ */ new Map();
|
|
447
|
+
const reverseEdges = /* @__PURE__ */ new Map();
|
|
448
|
+
const ensure = (fn) => {
|
|
449
|
+
const existing = mutable.get(fn);
|
|
450
|
+
if (existing) return existing;
|
|
451
|
+
const created = {
|
|
452
|
+
values: /* @__PURE__ */ new Set(),
|
|
453
|
+
unstableValues: /* @__PURE__ */ new Set()
|
|
454
|
+
};
|
|
455
|
+
mutable.set(fn, created);
|
|
456
|
+
return created;
|
|
457
|
+
};
|
|
458
|
+
for (const fn of graph.functions) ensure(fn);
|
|
459
|
+
for (const site of graph.calls) {
|
|
460
|
+
if (!site.owner) continue;
|
|
461
|
+
const direct = classifyDirectOperation(site.node);
|
|
462
|
+
if (direct) {
|
|
463
|
+
const summary = ensure(site.owner);
|
|
464
|
+
summary.values.add(direct.value);
|
|
465
|
+
if (direct.unstable) summary.unstableValues.add(direct.value);
|
|
466
|
+
}
|
|
467
|
+
if (!site.target) continue;
|
|
468
|
+
const incoming = reverseEdges.get(site.target) ?? [];
|
|
469
|
+
incoming.push({
|
|
470
|
+
owner: site.owner,
|
|
471
|
+
unstable: callIsUnstable(site.node, site.owner)
|
|
472
|
+
});
|
|
473
|
+
reverseEdges.set(site.target, incoming);
|
|
474
|
+
}
|
|
475
|
+
for (const site of graph.constructions) {
|
|
476
|
+
if (!site.owner) continue;
|
|
477
|
+
const direct = classifyDirectOperation(site.node);
|
|
478
|
+
if (!direct) continue;
|
|
479
|
+
const summary = ensure(site.owner);
|
|
480
|
+
summary.values.add(direct.value);
|
|
481
|
+
if (direct.unstable) summary.unstableValues.add(direct.value);
|
|
482
|
+
}
|
|
483
|
+
const queue = [...graph.functions];
|
|
484
|
+
const queued = new Set(queue);
|
|
485
|
+
for (let index = 0; index < queue.length; index += 1) {
|
|
486
|
+
const callee = queue[index];
|
|
487
|
+
queued.delete(callee);
|
|
488
|
+
const calleeSummary = ensure(callee);
|
|
489
|
+
for (const edge of reverseEdges.get(callee) ?? []) {
|
|
490
|
+
const ownerSummary = ensure(edge.owner);
|
|
491
|
+
let changed = addAll(ownerSummary.values, calleeSummary.values);
|
|
492
|
+
changed = addAll(ownerSummary.unstableValues, edge.unstable ? calleeSummary.values : calleeSummary.unstableValues) || changed;
|
|
493
|
+
if (changed && !queued.has(edge.owner)) {
|
|
494
|
+
queue.push(edge.owner);
|
|
495
|
+
queued.add(edge.owner);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return {
|
|
500
|
+
byFunction: mutable,
|
|
501
|
+
forCall(call) {
|
|
502
|
+
const target = graph.functionForCall(call);
|
|
503
|
+
return target ? mutable.get(target) ?? null : null;
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
//#endregion
|
|
240
508
|
//#region src/analyze/rules.ts
|
|
241
509
|
const SOURCE_BINDING_CACHE = /* @__PURE__ */ new WeakMap();
|
|
242
510
|
const SOURCE_FACT_CACHE = /* @__PURE__ */ new WeakMap();
|
|
@@ -341,8 +609,34 @@ function containingFunction(node) {
|
|
|
341
609
|
}
|
|
342
610
|
function isControlFlowAncestor(node, boundary) {
|
|
343
611
|
for (let current = node.parent; current && current !== boundary; current = current.parent) {
|
|
344
|
-
if (ts.isIfStatement(current)
|
|
345
|
-
if (ts.
|
|
612
|
+
if (ts.isIfStatement(current) && !isConstantCondition(current.expression)) return true;
|
|
613
|
+
if (ts.isConditionalExpression(current) && !isConstantCondition(current.condition) || ts.isSwitchStatement(current) || ts.isForStatement(current) || ts.isForInStatement(current) || ts.isForOfStatement(current) || ts.isWhileStatement(current) || ts.isDoStatement(current) || ts.isTryStatement(current)) return true;
|
|
614
|
+
if (ts.isBinaryExpression(current) && [ts.SyntaxKind.AmpersandAmpersandToken, ts.SyntaxKind.BarBarToken].includes(current.operatorToken.kind) && !isConstantCondition(current.left)) return true;
|
|
615
|
+
}
|
|
616
|
+
return hasConditionalEarlyExitBefore(node, boundary);
|
|
617
|
+
}
|
|
618
|
+
function containsFunctionExit(node) {
|
|
619
|
+
let found = false;
|
|
620
|
+
const walk = (candidate) => {
|
|
621
|
+
if (found || candidate !== node && ts.isFunctionLike(candidate)) return;
|
|
622
|
+
if (ts.isReturnStatement(candidate) || ts.isThrowStatement(candidate)) {
|
|
623
|
+
found = true;
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
ts.forEachChild(candidate, walk);
|
|
627
|
+
};
|
|
628
|
+
walk(node);
|
|
629
|
+
return found;
|
|
630
|
+
}
|
|
631
|
+
function hasConditionalEarlyExitBefore(node, boundary) {
|
|
632
|
+
let child = node;
|
|
633
|
+
for (let current = node.parent; current && current !== boundary; current = current.parent) {
|
|
634
|
+
if (ts.isBlock(current)) {
|
|
635
|
+
const statement = current.statements.find((candidate) => candidate === child || candidate.pos <= child.pos && child.end <= candidate.end);
|
|
636
|
+
const index = statement ? current.statements.indexOf(statement) : -1;
|
|
637
|
+
if (index > 0 && current.statements.slice(0, index).some((candidate) => ts.isIfStatement(candidate) && !isConstantCondition(candidate.expression) && containsFunctionExit(candidate))) return true;
|
|
638
|
+
}
|
|
639
|
+
child = current;
|
|
346
640
|
}
|
|
347
641
|
return false;
|
|
348
642
|
}
|
|
@@ -385,12 +679,31 @@ const stableRenderRule = {
|
|
|
385
679
|
description: "Render-scoped Askr primitives must have stable top-level call order.",
|
|
386
680
|
analyze(context) {
|
|
387
681
|
const diagnostics = [];
|
|
682
|
+
const wrapperSummary = summarizeTransitiveCalls(context, (call) => {
|
|
683
|
+
if (!ts.isCallExpression(call)) return null;
|
|
684
|
+
const name = canonicalCallName(call.expression, sourceBindings(call.getSourceFile()));
|
|
685
|
+
return name && (RENDER_SCOPED_CONCEPTS.has(name) || POSITIONAL_DATA_CONCEPTS.has(name)) ? {
|
|
686
|
+
value: name,
|
|
687
|
+
unstable: Boolean(containingFunction(call) && isControlFlowAncestor(call, containingFunction(call)))
|
|
688
|
+
} : null;
|
|
689
|
+
}, isControlFlowAncestor);
|
|
388
690
|
for (const sourceFile of context.sourceFiles) {
|
|
389
691
|
const bindings = sourceBindings(sourceFile);
|
|
390
692
|
visit(sourceFile, (node) => {
|
|
391
693
|
if (!ts.isCallExpression(node)) return;
|
|
392
694
|
const name = canonicalCallName(node.expression, bindings);
|
|
393
|
-
if (!name || !RENDER_SCOPED_CONCEPTS.has(name) && !POSITIONAL_DATA_CONCEPTS.has(name))
|
|
695
|
+
if (!name || !RENDER_SCOPED_CONCEPTS.has(name) && !POSITIONAL_DATA_CONCEPTS.has(name)) {
|
|
696
|
+
const summary = wrapperSummary.forCall(node);
|
|
697
|
+
const owner = containingFunction(node);
|
|
698
|
+
if (!summary || summary.values.size === 0 || !owner || !/^[A-Z]/.test(functionName(owner) ?? "") && !containsJsx(owner) || !isControlFlowAncestor(node, owner) && summary.unstableValues.size === 0) return;
|
|
699
|
+
const conditionalCallSite = isControlFlowAncestor(node, owner);
|
|
700
|
+
const conditionalWrapperInternals = summary.unstableValues.size > 0;
|
|
701
|
+
const concepts = [...conditionalWrapperInternals ? summary.unstableValues : summary.values].sort().join(", ");
|
|
702
|
+
const message = conditionalCallSite && conditionalWrapperInternals ? `${node.expression.getText()}() is called conditionally and transitively contains conditionally executed render-owned Askr APIs (${concepts}).` : conditionalCallSite ? `${node.expression.getText()}() is called conditionally and transitively calls render-owned Askr APIs (${concepts}).` : `${node.expression.getText()}() transitively contains conditionally executed render-owned Askr APIs (${concepts}).`;
|
|
703
|
+
const remediation = conditionalCallSite ? "Call the wrapper unconditionally at the top level and branch on its result or inputs." : "Make the render-owned call unconditional inside the wrapper and branch on its result or inputs.";
|
|
704
|
+
diagnostics.push(diagnostic(context, node.expression, this, message, remediation));
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
394
707
|
const owner = containingFunction(node);
|
|
395
708
|
if (!owner) {
|
|
396
709
|
if (POSITIONAL_DATA_CONCEPTS.has(name)) return;
|
|
@@ -403,14 +716,194 @@ const stableRenderRule = {
|
|
|
403
716
|
if (!ts.isJsxOpeningElement(node) && !ts.isJsxSelfClosingElement(node)) return;
|
|
404
717
|
const name = canonicalJsxName(node.tagName, bindings);
|
|
405
718
|
if (!name || !EAGER_CONTROL_CONCEPTS.has(name)) return;
|
|
406
|
-
|
|
407
|
-
const
|
|
408
|
-
|
|
719
|
+
const conditional = unstableControlConditional(node, bindings);
|
|
720
|
+
const owner = containingFunction(node);
|
|
721
|
+
const followsEarlyReturn = Boolean(owner && hasConditionalEarlyExitBefore(node, owner));
|
|
722
|
+
if (!conditional && !followsEarlyReturn) return;
|
|
723
|
+
const remediation = followsEarlyReturn ? name === "For" ? "Keep <For> mounted and represent the unavailable case with an empty each source." : name === "Show" ? "Keep <Show> mounted and move the condition into its when prop." : "Keep <Case> mounted and express the condition in its branches." : name === "Show" ? "Mount <Show> unconditionally and move the condition into its when prop." : "Replace the ternary or logical expression with a <Show> boundary.";
|
|
724
|
+
diagnostics.push(diagnostic(context, node.tagName, this, followsEarlyReturn ? `<${name}> is reached after a conditional early return, so its render position is unstable.` : `<${name}> is mounted behind a changing conditional, so its render position is unstable.`, remediation));
|
|
409
725
|
});
|
|
410
726
|
}
|
|
411
727
|
return diagnostics;
|
|
412
728
|
}
|
|
413
729
|
};
|
|
730
|
+
const TIMER_STARTS = /* @__PURE__ */ new Set([
|
|
731
|
+
"requestAnimationFrame",
|
|
732
|
+
"setInterval",
|
|
733
|
+
"setTimeout"
|
|
734
|
+
]);
|
|
735
|
+
const OBSERVER_CONSTRUCTORS = /* @__PURE__ */ new Set([
|
|
736
|
+
"IntersectionObserver",
|
|
737
|
+
"MutationObserver",
|
|
738
|
+
"ResizeObserver"
|
|
739
|
+
]);
|
|
740
|
+
const SUBSCRIPTION_CONSTRUCTORS = /* @__PURE__ */ new Set([
|
|
741
|
+
"BroadcastChannel",
|
|
742
|
+
"EventSource",
|
|
743
|
+
"WebSocket"
|
|
744
|
+
]);
|
|
745
|
+
const PLATFORM_EVENT_TARGET_TYPES = /^(?:AbortSignal|Document|EventTarget|HTMLElement|MediaQueryList|Window)(?:<.*>)?$/;
|
|
746
|
+
const PROJECT_SOURCE_FILES = /* @__PURE__ */ new WeakMap();
|
|
747
|
+
const CLEANUP_CALLS = /* @__PURE__ */ new WeakMap();
|
|
748
|
+
function projectDeclaredIdentifier(identifier, context) {
|
|
749
|
+
let symbol = context.checker.getSymbolAtLocation(identifier);
|
|
750
|
+
if (symbol && (symbol.flags & ts.SymbolFlags.Alias) !== 0) symbol = context.checker.getAliasedSymbol(symbol);
|
|
751
|
+
let sourceFiles = PROJECT_SOURCE_FILES.get(context.program);
|
|
752
|
+
if (!sourceFiles) {
|
|
753
|
+
sourceFiles = new Set(context.sourceFiles);
|
|
754
|
+
PROJECT_SOURCE_FILES.set(context.program, sourceFiles);
|
|
755
|
+
}
|
|
756
|
+
return symbol?.declarations?.some((declaration) => sourceFiles.has(declaration.getSourceFile())) ?? false;
|
|
757
|
+
}
|
|
758
|
+
function knownGlobalObject(expression, context) {
|
|
759
|
+
return ts.isIdentifier(expression) && [
|
|
760
|
+
"document",
|
|
761
|
+
"globalThis",
|
|
762
|
+
"window"
|
|
763
|
+
].includes(expression.text) && !projectDeclaredIdentifier(expression, context);
|
|
764
|
+
}
|
|
765
|
+
function typedPlatformEventTarget(expression, context) {
|
|
766
|
+
if (knownGlobalObject(expression, context)) return true;
|
|
767
|
+
if (!ts.isIdentifier(expression)) return false;
|
|
768
|
+
const symbol = context.checker.getSymbolAtLocation(expression);
|
|
769
|
+
return Boolean(symbol?.declarations?.some((declaration) => {
|
|
770
|
+
if (!ts.isVariableDeclaration(declaration) && !ts.isParameter(declaration) && !ts.isPropertyDeclaration(declaration)) return false;
|
|
771
|
+
return Boolean(declaration.type && PLATFORM_EVENT_TARGET_TYPES.test(declaration.type.getText()));
|
|
772
|
+
}));
|
|
773
|
+
}
|
|
774
|
+
function directGlobalCall(call, names, context) {
|
|
775
|
+
if (ts.isIdentifier(call.expression) && names.has(call.expression.text) && !projectDeclaredIdentifier(call.expression, context)) return call.expression.text;
|
|
776
|
+
if (ts.isPropertyAccessExpression(call.expression) && names.has(call.expression.name.text) && knownGlobalObject(call.expression.expression, context)) return call.expression.name.text;
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
function platformConstructorName(expression, context) {
|
|
780
|
+
if (ts.isIdentifier(expression) && !projectDeclaredIdentifier(expression, context)) return expression.text;
|
|
781
|
+
if (ts.isPropertyAccessExpression(expression) && knownGlobalObject(expression.expression, context)) return expression.name.text;
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
function renderSideEffect(node, context) {
|
|
785
|
+
if (ts.isNewExpression(node)) {
|
|
786
|
+
const name = platformConstructorName(node.expression, context);
|
|
787
|
+
if (name && OBSERVER_CONSTRUCTORS.has(name)) return "observer";
|
|
788
|
+
if (name && SUBSCRIPTION_CONSTRUCTORS.has(name)) return "subscription";
|
|
789
|
+
return null;
|
|
790
|
+
}
|
|
791
|
+
if (directGlobalCall(node, TIMER_STARTS, context)) return "timer";
|
|
792
|
+
if (ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "addEventListener" && typedPlatformEventTarget(node.expression.expression, context)) return "listener";
|
|
793
|
+
if (ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "watchPosition" && ts.isPropertyAccessExpression(node.expression.expression) && node.expression.expression.name.text === "geolocation" && ts.isIdentifier(node.expression.expression.expression) && node.expression.expression.expression.text === "navigator" && !projectDeclaredIdentifier(node.expression.expression.expression, context)) return "subscription";
|
|
794
|
+
return null;
|
|
795
|
+
}
|
|
796
|
+
function terminalCleanupFunction(callback, context) {
|
|
797
|
+
if (!("body" in callback) || !callback.body) return null;
|
|
798
|
+
let returned;
|
|
799
|
+
if (ts.isArrowFunction(callback) && !ts.isBlock(callback.body)) returned = callback.body;
|
|
800
|
+
else if (ts.isBlock(callback.body)) {
|
|
801
|
+
const last = callback.body.statements.at(-1);
|
|
802
|
+
if (last && ts.isReturnStatement(last)) returned = last.expression;
|
|
803
|
+
}
|
|
804
|
+
return resolvedFunction(returned, context);
|
|
805
|
+
}
|
|
806
|
+
function cleanupCalls(callback, context) {
|
|
807
|
+
const cached = CLEANUP_CALLS.get(callback);
|
|
808
|
+
if (cached) return cached;
|
|
809
|
+
const cleanup = terminalCleanupFunction(callback, context);
|
|
810
|
+
if (!cleanup) {
|
|
811
|
+
CLEANUP_CALLS.set(callback, []);
|
|
812
|
+
return [];
|
|
813
|
+
}
|
|
814
|
+
const calls = [];
|
|
815
|
+
const walk = (node) => {
|
|
816
|
+
if (node !== cleanup && ts.isFunctionLike(node)) return;
|
|
817
|
+
if (ts.isCallExpression(node)) calls.push(node);
|
|
818
|
+
ts.forEachChild(node, walk);
|
|
819
|
+
};
|
|
820
|
+
walk(cleanup);
|
|
821
|
+
CLEANUP_CALLS.set(callback, calls);
|
|
822
|
+
return calls;
|
|
823
|
+
}
|
|
824
|
+
function assignedEffectReference(node) {
|
|
825
|
+
const parent = node.parent;
|
|
826
|
+
if (ts.isVariableDeclaration(parent) && parent.initializer === node) return ts.isIdentifier(parent.name) ? parent.name : null;
|
|
827
|
+
if (ts.isBinaryExpression(parent) && parent.right === node && parent.operatorToken.kind === ts.SyntaxKind.EqualsToken) return parent.left;
|
|
828
|
+
return null;
|
|
829
|
+
}
|
|
830
|
+
function sameReference(left, right, context) {
|
|
831
|
+
if (!left || !right) return false;
|
|
832
|
+
let leftSymbol = context.checker.getSymbolAtLocation(left);
|
|
833
|
+
let rightSymbol = context.checker.getSymbolAtLocation(right);
|
|
834
|
+
if (leftSymbol && (leftSymbol.flags & ts.SymbolFlags.Alias) !== 0) leftSymbol = context.checker.getAliasedSymbol(leftSymbol);
|
|
835
|
+
if (rightSymbol && (rightSymbol.flags & ts.SymbolFlags.Alias) !== 0) rightSymbol = context.checker.getAliasedSymbol(rightSymbol);
|
|
836
|
+
return leftSymbol && rightSymbol ? leftSymbol === rightSymbol : left.getText() === right.getText();
|
|
837
|
+
}
|
|
838
|
+
function taskCleansDirectEffect(callback, node, effect, context) {
|
|
839
|
+
const cleanup = cleanupCalls(callback, context);
|
|
840
|
+
if (effect === "listener" && ts.isCallExpression(node)) {
|
|
841
|
+
if (!ts.isPropertyAccessExpression(node.expression) || node.expression.name.text !== "addEventListener") return false;
|
|
842
|
+
const startExpression = node.expression;
|
|
843
|
+
return cleanup.some((call) => ts.isPropertyAccessExpression(call.expression) && call.expression.name.text === "removeEventListener" && sameReference(call.expression.expression, startExpression.expression, context) && sameReference(call.arguments[0], node.arguments[0], context) && sameReference(call.arguments[1], node.arguments[1], context) && (node.arguments[2] === void 0 || sameReference(call.arguments[2], node.arguments[2], context)));
|
|
844
|
+
}
|
|
845
|
+
const handle = assignedEffectReference(node);
|
|
846
|
+
if (!handle) return false;
|
|
847
|
+
if (effect === "timer" && ts.isCallExpression(node)) {
|
|
848
|
+
const start = directGlobalCall(node, TIMER_STARTS, context);
|
|
849
|
+
const expected = start === "setInterval" ? "clearInterval" : start === "setTimeout" ? "clearTimeout" : start === "requestAnimationFrame" ? "cancelAnimationFrame" : null;
|
|
850
|
+
const expectedSet = expected ? /* @__PURE__ */ new Set([expected]) : /* @__PURE__ */ new Set();
|
|
851
|
+
return cleanup.some((call) => Boolean(directGlobalCall(call, expectedSet, context)) && sameReference(call.arguments[0], handle, context));
|
|
852
|
+
}
|
|
853
|
+
if (effect === "observer") return cleanup.some((call) => ts.isPropertyAccessExpression(call.expression) && call.expression.name.text === "disconnect" && sameReference(call.expression.expression, handle, context));
|
|
854
|
+
if (effect === "subscription") return cleanup.some((call) => {
|
|
855
|
+
if (ts.isPropertyAccessExpression(call.expression) && (call.expression.name.text === "close" || call.expression.name.text === "unsubscribe") && sameReference(call.expression.expression, handle, context)) return true;
|
|
856
|
+
return ts.isCallExpression(node) && ts.isPropertyAccessExpression(call.expression) && call.expression.name.text === "clearWatch" && ts.isPropertyAccessExpression(call.expression.expression) && call.expression.expression.name.text === "geolocation" && ts.isIdentifier(call.expression.expression.expression) && call.expression.expression.expression.text === "navigator" && !projectDeclaredIdentifier(call.expression.expression.expression, context) && sameReference(call.arguments[0], handle, context);
|
|
857
|
+
});
|
|
858
|
+
return false;
|
|
859
|
+
}
|
|
860
|
+
function isRenderFunction(owner) {
|
|
861
|
+
return /^[A-Z]/.test(functionName(owner) ?? "") || containsJsx(owner);
|
|
862
|
+
}
|
|
863
|
+
const renderSideEffectRule = {
|
|
864
|
+
id: "askr/render-side-effect",
|
|
865
|
+
category: "correctness",
|
|
866
|
+
severity: "error",
|
|
867
|
+
description: "Platform side effects started during render require lifecycle-owned cleanup.",
|
|
868
|
+
analyze(context) {
|
|
869
|
+
const diagnostics = [];
|
|
870
|
+
const graph = localCallGraph(context);
|
|
871
|
+
const summary = summarizeTransitiveCalls(context, (node) => {
|
|
872
|
+
const effect = renderSideEffect(node, context);
|
|
873
|
+
return effect ? { value: effect } : null;
|
|
874
|
+
}, () => false);
|
|
875
|
+
const lifecycleCallbacks = /* @__PURE__ */ new Set();
|
|
876
|
+
for (const site of graph.calls) {
|
|
877
|
+
if (canonicalCallName(site.node.expression, sourceBindings(site.node.getSourceFile())) !== "task") continue;
|
|
878
|
+
const argument = site.node.arguments[0];
|
|
879
|
+
const callback = resolvedFunction(argument, context) ?? (argument ? graph.functionForExpression(argument) : null);
|
|
880
|
+
if (callback) lifecycleCallbacks.add(callback);
|
|
881
|
+
}
|
|
882
|
+
const report = (node, effects, direct = false) => {
|
|
883
|
+
const owner = containingFunction(node);
|
|
884
|
+
if (!owner) return;
|
|
885
|
+
const managed = lifecycleCallbacks.has(owner);
|
|
886
|
+
if (managed && direct && [...effects].every((effect) => taskCleansDirectEffect(owner, node, effect, context))) return;
|
|
887
|
+
if (!managed && !isRenderFunction(owner)) return;
|
|
888
|
+
const effectNames = [...effects].sort().join(", ");
|
|
889
|
+
diagnostics.push(diagnostic(context, node.expression, this, managed ? `This task starts ${effectNames} side effects without returning matching cleanup.` : `${node.expression.getText()} starts unmanaged ${effectNames} side effects during render.`, "Start the effect in task() and return cleanup that clears, disconnects, removes, closes, or unsubscribes it."));
|
|
890
|
+
};
|
|
891
|
+
for (const site of graph.calls) {
|
|
892
|
+
const direct = renderSideEffect(site.node, context);
|
|
893
|
+
if (direct) {
|
|
894
|
+
report(site.node, /* @__PURE__ */ new Set([direct]), true);
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
const wrapper = summary.forCall(site.node);
|
|
898
|
+
if (wrapper && wrapper.values.size > 0) report(site.node, wrapper.values);
|
|
899
|
+
}
|
|
900
|
+
for (const site of graph.constructions) {
|
|
901
|
+
const direct = renderSideEffect(site.node, context);
|
|
902
|
+
if (direct) report(site.node, /* @__PURE__ */ new Set([direct]), true);
|
|
903
|
+
}
|
|
904
|
+
return diagnostics;
|
|
905
|
+
}
|
|
906
|
+
};
|
|
414
907
|
function collectStateBindings(sourceFile, bindings) {
|
|
415
908
|
const getters = /* @__PURE__ */ new Set();
|
|
416
909
|
const setters = /* @__PURE__ */ new Set();
|
|
@@ -660,7 +1153,10 @@ const preferForRule = {
|
|
|
660
1153
|
return diagnostics;
|
|
661
1154
|
}
|
|
662
1155
|
};
|
|
1156
|
+
const CONTAINS_JSX = /* @__PURE__ */ new WeakMap();
|
|
663
1157
|
function containsJsx(node) {
|
|
1158
|
+
const cached = CONTAINS_JSX.get(node);
|
|
1159
|
+
if (cached !== void 0) return cached;
|
|
664
1160
|
let found = false;
|
|
665
1161
|
const walk = (candidate) => {
|
|
666
1162
|
if (found) return;
|
|
@@ -671,6 +1167,7 @@ function containsJsx(node) {
|
|
|
671
1167
|
ts.forEachChild(candidate, walk);
|
|
672
1168
|
};
|
|
673
1169
|
walk(node);
|
|
1170
|
+
CONTAINS_JSX.set(node, found);
|
|
674
1171
|
return found;
|
|
675
1172
|
}
|
|
676
1173
|
function isAsyncFunction(node) {
|
|
@@ -1269,6 +1766,7 @@ const ANALYZE_RULES = [
|
|
|
1269
1766
|
}
|
|
1270
1767
|
},
|
|
1271
1768
|
stableRenderRule,
|
|
1769
|
+
renderSideEffectRule,
|
|
1272
1770
|
stateAccessRule,
|
|
1273
1771
|
stateRenderWriteRule,
|
|
1274
1772
|
resourceCancellationRule,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as inspectBundledSkills } from "./skills-CSGdAZHN.js";
|
|
2
2
|
import { discoverWorkspaceProject } from "./discovery-DUDrZCIC.js";
|
|
3
|
-
import { analysisHasBlockingFindings, runAnalysis } from "./runner-
|
|
3
|
+
import { analysisHasBlockingFindings, runAnalysis } from "./runner-BSs9Ow3t.js";
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
@@ -27,151 +27,154 @@ export default function AdminHomePage() {
|
|
|
27
27
|
const operations = resource(({ signal }) => loadOperations({ signal }), []);
|
|
28
28
|
const snapshot = operations.value;
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
<
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
);
|
|
41
|
-
}
|
|
30
|
+
const errorState = (
|
|
31
|
+
<Section>
|
|
32
|
+
<EmptyState
|
|
33
|
+
icon={<AlertCircleIcon size={24} aria-hidden="true" />}
|
|
34
|
+
title="Operations could not load"
|
|
35
|
+
description="The dashboard keeps failures recoverable. Retry the owning resource instead of hiding the error in a toast."
|
|
36
|
+
actions={<Button onPress={() => operations.refresh()}>Retry</Button>}
|
|
37
|
+
/>
|
|
38
|
+
</Section>
|
|
39
|
+
);
|
|
42
40
|
|
|
43
41
|
return (
|
|
44
|
-
<
|
|
45
|
-
<
|
|
46
|
-
<
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
42
|
+
<Show when={!operations.error || snapshot} fallback={errorState}>
|
|
43
|
+
<Stack gap="5">
|
|
44
|
+
<section class="page-heading">
|
|
45
|
+
<Stack gap="2">
|
|
46
|
+
<Badge>projection v{snapshot?.version ?? '...'}</Badge>
|
|
47
|
+
<h1>Workspace home</h1>
|
|
48
|
+
<p class="lead">
|
|
49
|
+
A consistency-aware dashboard for agent runs, queue health, and
|
|
50
|
+
event-sourced read models.
|
|
51
|
+
</p>
|
|
52
|
+
</Stack>
|
|
53
|
+
<Inline gap="2" align="center">
|
|
54
|
+
{operations.pending && snapshot ? <Badge>refreshing</Badge> : null}
|
|
55
|
+
<Button variant="secondary" onPress={() => operations.refresh()}>
|
|
56
|
+
<RefreshCwIcon size={14} aria-hidden="true" /> Refresh
|
|
57
|
+
</Button>
|
|
58
|
+
</Inline>
|
|
59
|
+
</section>
|
|
61
60
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
{operations.pending && !snapshot ? (
|
|
62
|
+
<Block gap="md">
|
|
63
|
+
<Skeleton style="height: 8rem" />
|
|
64
|
+
<Skeleton style="height: 8rem" />
|
|
65
|
+
<Skeleton style="height: 8rem" />
|
|
66
|
+
</Block>
|
|
67
|
+
) : null}
|
|
69
68
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
69
|
+
<Show when={snapshot}>
|
|
70
|
+
{(currentSnapshot) => (
|
|
71
|
+
<>
|
|
72
|
+
<Block gap="md" class="metric-grid">
|
|
73
|
+
<For
|
|
74
|
+
each={currentSnapshot.metrics}
|
|
75
|
+
by={(metric) => metric.label}
|
|
76
|
+
>
|
|
77
|
+
{(metric) => (
|
|
78
|
+
<MetricCard
|
|
79
|
+
label={metric.label}
|
|
80
|
+
value={metric.value}
|
|
81
|
+
trend={metric.trend}
|
|
82
|
+
/>
|
|
83
|
+
)}
|
|
84
|
+
</For>
|
|
85
|
+
</Block>
|
|
86
|
+
|
|
87
|
+
<Block gap="md" align="stretch" class="chart-grid">
|
|
88
|
+
<Card>
|
|
89
|
+
<CardHeader>
|
|
90
|
+
<CardTitle>Run throughput</CardTitle>
|
|
91
|
+
<CardDescription>
|
|
92
|
+
Accepted commands by work type.
|
|
93
|
+
</CardDescription>
|
|
94
|
+
</CardHeader>
|
|
95
|
+
<CardContent>
|
|
96
|
+
<OperationsPlot.Root
|
|
97
|
+
data={currentSnapshot.throughput}
|
|
98
|
+
rowKey="label"
|
|
99
|
+
label="Run throughput"
|
|
100
|
+
description="Accepted commands by work type."
|
|
101
|
+
>
|
|
102
|
+
<OperationsPlot.Bar x="label" y="value" />
|
|
103
|
+
</OperationsPlot.Root>
|
|
104
|
+
</CardContent>
|
|
105
|
+
</Card>
|
|
106
|
+
<Card>
|
|
107
|
+
<CardHeader>
|
|
108
|
+
<CardTitle>Projection lag</CardTitle>
|
|
109
|
+
<CardDescription>
|
|
110
|
+
Lower is better; stale states stay visible.
|
|
111
|
+
</CardDescription>
|
|
112
|
+
</CardHeader>
|
|
113
|
+
<CardContent>
|
|
114
|
+
<OperationsPlot.Root
|
|
115
|
+
data={currentSnapshot.lag}
|
|
116
|
+
rowKey="label"
|
|
117
|
+
label="Projection lag"
|
|
118
|
+
description="Projection lag over the last hour."
|
|
119
|
+
>
|
|
120
|
+
<OperationsPlot.Line x="label" y="value" />
|
|
121
|
+
<OperationsPlot.Point x="label" y="value" />
|
|
122
|
+
</OperationsPlot.Root>
|
|
123
|
+
</CardContent>
|
|
124
|
+
</Card>
|
|
125
|
+
</Block>
|
|
126
|
+
|
|
127
|
+
{currentSnapshot.consistency !== 'fresh' ? (
|
|
128
|
+
<Alert variant="warning">
|
|
129
|
+
Read models are {currentSnapshot.consistency}. Last processed
|
|
130
|
+
event is {currentSnapshot.lastEventId}.
|
|
131
|
+
</Alert>
|
|
132
|
+
) : null}
|
|
84
133
|
|
|
85
|
-
<Block gap="md" align="stretch" class="chart-grid">
|
|
86
|
-
<Card>
|
|
87
|
-
<CardHeader>
|
|
88
|
-
<CardTitle>Run throughput</CardTitle>
|
|
89
|
-
<CardDescription>
|
|
90
|
-
Accepted commands by work type.
|
|
91
|
-
</CardDescription>
|
|
92
|
-
</CardHeader>
|
|
93
|
-
<CardContent>
|
|
94
|
-
<OperationsPlot.Root
|
|
95
|
-
data={currentSnapshot.throughput}
|
|
96
|
-
rowKey="label"
|
|
97
|
-
label="Run throughput"
|
|
98
|
-
description="Accepted commands by work type."
|
|
99
|
-
>
|
|
100
|
-
<OperationsPlot.Bar x="label" y="value" />
|
|
101
|
-
</OperationsPlot.Root>
|
|
102
|
-
</CardContent>
|
|
103
|
-
</Card>
|
|
104
134
|
<Card>
|
|
105
135
|
<CardHeader>
|
|
106
|
-
<CardTitle>
|
|
136
|
+
<CardTitle>Recent agent runs</CardTitle>
|
|
107
137
|
<CardDescription>
|
|
108
|
-
|
|
138
|
+
Run state is modeled as product state, not a single loading
|
|
139
|
+
boolean.
|
|
109
140
|
</CardDescription>
|
|
110
141
|
</CardHeader>
|
|
111
142
|
<CardContent>
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
143
|
+
<div class="run-table-wrap">
|
|
144
|
+
<table class="run-table">
|
|
145
|
+
<thead>
|
|
146
|
+
<tr>
|
|
147
|
+
<th>Run</th>
|
|
148
|
+
<th>Status</th>
|
|
149
|
+
<th>Owner</th>
|
|
150
|
+
<th>Updated</th>
|
|
151
|
+
</tr>
|
|
152
|
+
</thead>
|
|
153
|
+
<tbody>
|
|
154
|
+
<For each={currentSnapshot.runs} by={(run) => run.id}>
|
|
155
|
+
{(run) => (
|
|
156
|
+
<tr>
|
|
157
|
+
<td>
|
|
158
|
+
<strong>{run.title}</strong>
|
|
159
|
+
<span>{run.id}</span>
|
|
160
|
+
</td>
|
|
161
|
+
<td>
|
|
162
|
+
<StatusBadge status={run.status} />
|
|
163
|
+
</td>
|
|
164
|
+
<td>{run.owner}</td>
|
|
165
|
+
<td>{formatRelativeTime(run.updatedAt)}</td>
|
|
166
|
+
</tr>
|
|
167
|
+
)}
|
|
168
|
+
</For>
|
|
169
|
+
</tbody>
|
|
170
|
+
</table>
|
|
171
|
+
</div>
|
|
121
172
|
</CardContent>
|
|
122
173
|
</Card>
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
event is {currentSnapshot.lastEventId}.
|
|
129
|
-
</Alert>
|
|
130
|
-
) : null}
|
|
131
|
-
|
|
132
|
-
<Card>
|
|
133
|
-
<CardHeader>
|
|
134
|
-
<CardTitle>Recent agent runs</CardTitle>
|
|
135
|
-
<CardDescription>
|
|
136
|
-
Run state is modeled as product state, not a single loading
|
|
137
|
-
boolean.
|
|
138
|
-
</CardDescription>
|
|
139
|
-
</CardHeader>
|
|
140
|
-
<CardContent>
|
|
141
|
-
<div class="run-table-wrap">
|
|
142
|
-
<table class="run-table">
|
|
143
|
-
<thead>
|
|
144
|
-
<tr>
|
|
145
|
-
<th>Run</th>
|
|
146
|
-
<th>Status</th>
|
|
147
|
-
<th>Owner</th>
|
|
148
|
-
<th>Updated</th>
|
|
149
|
-
</tr>
|
|
150
|
-
</thead>
|
|
151
|
-
<tbody>
|
|
152
|
-
<For each={currentSnapshot.runs} by={(run) => run.id}>
|
|
153
|
-
{(run) => (
|
|
154
|
-
<tr>
|
|
155
|
-
<td>
|
|
156
|
-
<strong>{run.title}</strong>
|
|
157
|
-
<span>{run.id}</span>
|
|
158
|
-
</td>
|
|
159
|
-
<td>
|
|
160
|
-
<StatusBadge status={run.status} />
|
|
161
|
-
</td>
|
|
162
|
-
<td>{run.owner}</td>
|
|
163
|
-
<td>{formatRelativeTime(run.updatedAt)}</td>
|
|
164
|
-
</tr>
|
|
165
|
-
)}
|
|
166
|
-
</For>
|
|
167
|
-
</tbody>
|
|
168
|
-
</table>
|
|
169
|
-
</div>
|
|
170
|
-
</CardContent>
|
|
171
|
-
</Card>
|
|
172
|
-
</>
|
|
173
|
-
)}
|
|
174
|
-
</Show>
|
|
175
|
-
</Stack>
|
|
174
|
+
</>
|
|
175
|
+
)}
|
|
176
|
+
</Show>
|
|
177
|
+
</Stack>
|
|
178
|
+
</Show>
|
|
176
179
|
);
|
|
177
180
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { For } from '@askrjs/askr/control';
|
|
1
|
+
import { Case, For, Match } from '@askrjs/askr/control';
|
|
2
2
|
import { Skeleton } from '@askrjs/themes/components';
|
|
3
3
|
import EmptyState from './empty-state';
|
|
4
4
|
import { joinClasses } from '../utils/join-classes';
|
|
@@ -22,38 +22,8 @@ export default function DataTable<Row>(props: {
|
|
|
22
22
|
emptyTitle?: string;
|
|
23
23
|
emptyDescription?: string;
|
|
24
24
|
}) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
<EmptyState title="Could not load table" description={props.errorText} />
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (props.isLoading) {
|
|
32
|
-
return (
|
|
33
|
-
<div
|
|
34
|
-
class={joinClasses('panel stack-sm', props.class)}
|
|
35
|
-
aria-hidden="true"
|
|
36
|
-
>
|
|
37
|
-
<Skeleton class="skeleton-line" />
|
|
38
|
-
<Skeleton class="skeleton-line" />
|
|
39
|
-
<Skeleton class="skeleton-line" />
|
|
40
|
-
</div>
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (props.rows().length === 0) {
|
|
45
|
-
return (
|
|
46
|
-
<EmptyState
|
|
47
|
-
title={props.emptyTitle ?? 'No rows found'}
|
|
48
|
-
description={
|
|
49
|
-
props.emptyDescription ??
|
|
50
|
-
'Try changing filters or adding new records.'
|
|
51
|
-
}
|
|
52
|
-
/>
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return (
|
|
25
|
+
const rows = props.rows();
|
|
26
|
+
const table = (
|
|
57
27
|
<div class={joinClasses('table-wrap', props.class)}>
|
|
58
28
|
<table class={props.tableClass}>
|
|
59
29
|
<thead>
|
|
@@ -79,4 +49,34 @@ export default function DataTable<Row>(props: {
|
|
|
79
49
|
</table>
|
|
80
50
|
</div>
|
|
81
51
|
);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Case fallback={table}>
|
|
55
|
+
<Match when={props.errorText}>
|
|
56
|
+
<EmptyState
|
|
57
|
+
title="Could not load table"
|
|
58
|
+
description={props.errorText ?? 'The table could not be loaded.'}
|
|
59
|
+
/>
|
|
60
|
+
</Match>
|
|
61
|
+
<Match when={props.isLoading}>
|
|
62
|
+
<div
|
|
63
|
+
class={joinClasses('panel stack-sm', props.class)}
|
|
64
|
+
aria-hidden="true"
|
|
65
|
+
>
|
|
66
|
+
<Skeleton class="skeleton-line" />
|
|
67
|
+
<Skeleton class="skeleton-line" />
|
|
68
|
+
<Skeleton class="skeleton-line" />
|
|
69
|
+
</div>
|
|
70
|
+
</Match>
|
|
71
|
+
<Match when={rows.length === 0}>
|
|
72
|
+
<EmptyState
|
|
73
|
+
title={props.emptyTitle ?? 'No rows found'}
|
|
74
|
+
description={
|
|
75
|
+
props.emptyDescription ??
|
|
76
|
+
'Try changing filters or adding new records.'
|
|
77
|
+
}
|
|
78
|
+
/>
|
|
79
|
+
</Match>
|
|
80
|
+
</Case>
|
|
81
|
+
);
|
|
82
82
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askrjs/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.13",
|
|
4
4
|
"description": "Unified CLI for the Askr platform",
|
|
5
5
|
"homepage": "https://github.com/askrjs/askr-cli#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"clean": "npx rimraf dist node_modules",
|
|
37
37
|
"build": "vp pack",
|
|
38
38
|
"dev": "vp pack --watch",
|
|
39
|
+
"analyze": "tsx src/bin/cli.ts analyze --check",
|
|
39
40
|
"test": "vp test run -c vitest.config.ts",
|
|
40
41
|
"test:coverage": "vp test run -c vitest.config.ts --coverage",
|
|
41
42
|
"fmt": "vp fmt .",
|
|
@@ -47,7 +48,7 @@
|
|
|
47
48
|
"bench": "npm run build --silent && npm run bench:analyze && node --import tsx benchmarks/cli.mjs --gate",
|
|
48
49
|
"bench:analyze": "vp test bench --run -c vitest.bench.config.ts",
|
|
49
50
|
"bench:json": "npm run build --silent && node --import tsx benchmarks/cli.mjs --gate --json",
|
|
50
|
-
"check": "npm run lint && npm run typecheck && npm run test:coverage && npm run build && npm run test:publint && npm run pack:check",
|
|
51
|
+
"check": "npm run analyze && npm run lint && npm run typecheck && npm run test:coverage && npm run build && npm run test:publint && npm run pack:check",
|
|
51
52
|
"prepack": "npm run build",
|
|
52
53
|
"prepublishOnly": "npm run check && npm run test:templates"
|
|
53
54
|
},
|
|
@@ -86,5 +87,12 @@
|
|
|
86
87
|
},
|
|
87
88
|
"engines": {
|
|
88
89
|
"node": "^20.19.0 || >=22.12.0"
|
|
90
|
+
},
|
|
91
|
+
"askr": {
|
|
92
|
+
"analyze": {
|
|
93
|
+
"exclude": [
|
|
94
|
+
"templates/**"
|
|
95
|
+
]
|
|
96
|
+
}
|
|
89
97
|
}
|
|
90
98
|
}
|