@crvouga/atlas 1.20.0 → 1.22.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.
Files changed (114) hide show
  1. package/CHANGELOG.md +15 -1
  2. package/dist/core/src/cli.d.ts +2 -2
  3. package/dist/core/src/cli.js +7 -1
  4. package/dist/critics/src/catalog-client.d.ts +91 -0
  5. package/dist/critics/src/catalog-client.js +194 -0
  6. package/dist/critics/src/cli-calibrate.d.ts +4 -0
  7. package/dist/critics/src/cli-calibrate.js +211 -0
  8. package/dist/critics/src/cli-common.d.ts +108 -0
  9. package/dist/critics/src/cli-common.js +155 -0
  10. package/dist/critics/src/cli-review.d.ts +4 -0
  11. package/dist/critics/src/cli-review.js +174 -0
  12. package/dist/critics/src/cli-watch.d.ts +4 -0
  13. package/dist/critics/src/cli-watch.js +191 -0
  14. package/dist/critics/src/cli.d.ts +13 -0
  15. package/dist/critics/src/cli.js +490 -0
  16. package/dist/critics/src/index.d.ts +3 -0
  17. package/dist/critics/src/index.js +3 -0
  18. package/dist/critics/src/judging.d.ts +8 -0
  19. package/dist/critics/src/llm.d.ts +120 -0
  20. package/dist/critics/src/llm.js +266 -0
  21. package/dist/critics/src/marks.d.ts +74 -0
  22. package/dist/critics/src/marks.js +76 -0
  23. package/dist/critics/src/registry.d.ts +125 -0
  24. package/dist/critics/src/registry.js +203 -0
  25. package/dist/critics/src/spool.d.ts +32 -0
  26. package/dist/critics/src/spool.js +129 -0
  27. package/dist/critics/src/uiclip.d.ts +82 -0
  28. package/dist/critics/src/uiclip.js +167 -0
  29. package/dist/reports/src/compose.d.ts +23 -1
  30. package/dist/reports/src/compose.js +29 -19
  31. package/dist/reports/src/critique-review.d.ts +111 -0
  32. package/dist/reports/src/critique-review.js +123 -0
  33. package/dist/reports/src/critique-row.d.ts +24 -0
  34. package/dist/reports/src/critique-row.js +39 -0
  35. package/dist/reports/src/evidence-types.d.ts +5 -0
  36. package/dist/reports/src/layer-merge.js +5 -0
  37. package/dist/reports/src/report-cli.d.ts +13 -0
  38. package/dist/reports/src/report-cli.js +6 -4
  39. package/dist/reports/src/report-index.d.ts +6 -0
  40. package/dist/reports/src/report-index.js +12 -0
  41. package/dist/reports/src/ui-catalog-cli.d.ts +10 -0
  42. package/dist/reports/src/ui-catalog-cli.js +391 -0
  43. package/dist/reports/src/ui-catalog-diff.d.ts +104 -0
  44. package/dist/reports/src/ui-catalog-diff.js +130 -0
  45. package/dist/reports/src/ui-catalog-entries.d.ts +147 -0
  46. package/dist/reports/src/ui-catalog-entries.js +190 -0
  47. package/dist/reports/src/ui-catalog-export.d.ts +82 -0
  48. package/dist/reports/src/ui-catalog-export.js +221 -0
  49. package/dist/reports/src/ui-catalog-publish.js +2 -2
  50. package/dist/reports/src/ui-catalog.d.ts +2 -0
  51. package/dist/reports/src/ui-catalog.js +1 -1
  52. package/dist/reports/src/ui-state.d.ts +2 -0
  53. package/dist/reports/src/ui-state.js +2 -2
  54. package/dist/schema/src/api.d.ts +653 -1
  55. package/dist/schema/src/api.js +44 -0
  56. package/dist/schema/src/cli-results.d.ts +991 -1
  57. package/dist/schema/src/cli-results.js +265 -1
  58. package/dist/schema/src/critique.d.ts +31 -0
  59. package/dist/schema/src/critique.js +26 -0
  60. package/dist/schema/src/server-config.d.ts +3 -3
  61. package/dist/server/src/app.js +28 -1
  62. package/dist/server/src/openapi.js +19 -2
  63. package/docs/agents.md +16 -2
  64. package/docs/critics.md +273 -9
  65. package/docs/extending.md +1 -1
  66. package/docs/reference/cli.md +126 -1
  67. package/docs/reference/core.md +1 -1
  68. package/docs/reference/critics.md +1131 -29
  69. package/docs/reference/http-api.md +40 -0
  70. package/docs/reference/reports.md +855 -0
  71. package/docs/reference/schema.md +197 -1
  72. package/docs/report-contract.md +2 -1
  73. package/docs/server.md +2 -0
  74. package/docs/ui-catalog.md +110 -13
  75. package/docs/ui.md +69 -2
  76. package/llms-full.txt +636 -26
  77. package/llms.txt +1 -1
  78. package/package.json +9 -1
  79. package/packages/critics/README.md +1 -0
  80. package/packages/schema/schemas/critic-calibrate-result.schema.json +274 -0
  81. package/packages/schema/schemas/critic-doctor-result.schema.json +74 -0
  82. package/packages/schema/schemas/critic-list-result.schema.json +90 -0
  83. package/packages/schema/schemas/critic-review-result.schema.json +397 -0
  84. package/packages/schema/schemas/critic-run-result.schema.json +249 -0
  85. package/packages/schema/schemas/critic-setup-result.schema.json +34 -0
  86. package/packages/schema/schemas/critic-watch-result.schema.json +211 -0
  87. package/packages/schema/schemas/critique-mark.schema.json +81 -0
  88. package/packages/schema/schemas/effective-delta.schema.json +337 -0
  89. package/packages/schema/schemas/effective-evidence.schema.json +337 -0
  90. package/packages/schema/schemas/layer-rows.schema.json +337 -0
  91. package/packages/schema/schemas/ui-catalog-diff-result.schema.json +201 -0
  92. package/packages/schema/schemas/ui-catalog-export-result.schema.json +83 -0
  93. package/packages/schema/schemas/ui-catalog-publish-result.schema.json +116 -0
  94. package/packages/schema/schemas/ui-catalog-status.schema.json +197 -0
  95. package/packages/server/openapi.json +6755 -4738
  96. package/packages/visualizer/src/app/AppShell.tsx +3 -1
  97. package/packages/visualizer/src/app/router.tsx +20 -7
  98. package/packages/visualizer/src/catalog/Picture.tsx +32 -0
  99. package/packages/visualizer/src/catalog/UiCatalogCompare.tsx +289 -0
  100. package/packages/visualizer/src/catalog/UiCatalogPage.tsx +18 -28
  101. package/packages/visualizer/src/catalog/history.ts +61 -0
  102. package/packages/visualizer/src/catalog/search.ts +14 -3
  103. package/packages/visualizer/src/components/Header.tsx +16 -4
  104. package/packages/visualizer/src/critiques/CritiquesPage.tsx +402 -0
  105. package/packages/visualizer/src/critiques/FindingOverlay.tsx +50 -0
  106. package/packages/visualizer/src/critiques/ranking.ts +54 -0
  107. package/packages/visualizer/src/critiques/search.ts +41 -0
  108. package/packages/visualizer/src/evidence/applicability-view.tsx +11 -7
  109. package/packages/visualizer/src/evidence/behaviors-page.tsx +22 -15
  110. package/packages/visualizer/src/evidence/evidence-context.tsx +2 -1
  111. package/packages/visualizer/src/explorer/Explorer.tsx +5 -0
  112. package/packages/visualizer/src/explorer/TestDetail.tsx +4 -1
  113. package/packages/visualizer/src/explorer/parts.tsx +19 -1
  114. package/packages/visualizer/src/explorer/playlist.tsx +11 -7
@@ -18,7 +18,8 @@ import { useDocSearch } from '../find/doc-search';
18
18
  import { passesFilters } from '../find/query';
19
19
  import type { BehaviorsSearch } from '../app/router';
20
20
  import { specKind } from '../spec-kind';
21
- import { ApplicabilitySummary } from './applicability-view';
21
+ import { needsAttention, type ReleaseIdentity } from './applicability';
22
+ import { ApplicabilityControls, ApplicabilitySummary, useApplicabilityState, type ApplicabilityState } from './applicability-view';
22
23
  import { behaviorDoc, indexBehaviors, type IndexedBehavior } from './behavior-index';
23
24
  import { BehaviorPanel, behaviorSteps, CoverageBadge, Player, Provenance, RevisionLine } from './behavior-panel';
24
25
  import { forgetReturn, recallReturn, rememberReturn, scopeKey } from './behavior-return';
@@ -36,29 +37,29 @@ const PAGE = 40;
36
37
  const NO_DOMAIN = 'No domain declared';
37
38
 
38
39
  /** Reads every feature and files its behaviors; `pending` while features are still being read. */
39
- function useBehaviorIndex() {
40
+ function useBehaviorIndex(release: ReleaseIdentity) {
40
41
  const evidence = useEvidence();
41
42
  const gherkin = specKind('gherkin')!;
42
43
  const files = useMemo(() => evidence.files.filter(gherkin.accepts), [evidence.files, gherkin]);
43
44
  const { texts, pending } = useSpecTexts(files, true, false);
44
45
  const index = useMemo(
45
- () => indexBehaviors({ files, texts, specs: evidence.specs, view: evidence.view, evidence: evidence.evidence }),
46
- [files, texts, evidence.specs, evidence.view, evidence.evidence]
46
+ () => indexBehaviors({ files, texts, specs: evidence.specs, view: evidence.view, evidence: evidence.evidence, release }),
47
+ [files, texts, evidence.specs, evidence.view, evidence.evidence, release]
47
48
  );
48
49
  return { index, pending: pending || evidence.filesPending, filesError: evidence.filesError };
49
50
  }
50
51
 
51
52
  /** What the search and coverage filter leave of the project's behaviors, best match first when words were typed. */
52
- function useListing(entries: readonly IndexedBehavior[], search: BehaviorsSearch) {
53
+ function useListing(entries: readonly IndexedBehavior[], search: BehaviorsSearch, onlyStale: boolean) {
53
54
  const docs = useMemo(() => new Map(entries.map((entry) => [entry.id, behaviorDoc(entry)] as const)), [entries]);
54
55
  const list = useMemo(() => [...docs.values()], [docs]);
55
56
  const found = useDocSearch(list, useDeferredValue(search.q));
56
57
  const scoped = useMemo(() => {
57
- const kept = entries.filter((entry) => (!found.hits || found.hits.has(entry.id)) && passesFilters(docs.get(entry.id)?.facets, found.parsed.filters));
58
+ const kept = entries.filter((entry) => (!found.hits || found.hits.has(entry.id)) && (!onlyStale || needsAttention(entry.assessment)) && passesFilters(docs.get(entry.id)?.facets, found.parsed.filters));
58
59
  if (!found.hits) return kept;
59
60
  const order = new Map([...found.hits.keys()].map((id, at) => [id, at] as const));
60
61
  return [...kept].sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0));
61
- }, [entries, docs, found.hits, found.parsed]);
62
+ }, [entries, docs, found.hits, found.parsed, onlyStale]);
62
63
  const counts = useMemo(() => coverageCounts(scoped.map((entry) => entry.behavior)), [scoped]);
63
64
  const shown = useMemo(() => (search.coverage ? scoped.filter((entry) => entry.behavior.state === search.coverage) : scoped), [scoped, search.coverage]);
64
65
  return { found, scoped, shown, counts, ranked: found.hits !== null };
@@ -216,7 +217,7 @@ function BehaviorList({ listing, selected, limit, onMore }: { listing: Listing;
216
217
  }
217
218
 
218
219
  /** The index: search, coverage filter, the list, and beside it the Read / Watch / Inspect panel of the selected behavior. */
219
- function Index({ listing, search, index, pending, filesError }: { listing: Listing; search: BehaviorsSearch; index: ReturnType<typeof useBehaviorIndex>['index']; pending: boolean; filesError: boolean }) {
220
+ function Index({ listing, search, index, pending, filesError, applicability }: { listing: Listing; search: BehaviorsSearch; index: ReturnType<typeof useBehaviorIndex>['index']; pending: boolean; filesError: boolean; applicability: ApplicabilityState }) {
220
221
  const navigate = useNavigate({ from: '/behaviors' });
221
222
  const scope = scopeKey(search);
222
223
  const [draft, setDraft] = useState(search.q ?? '');
@@ -250,6 +251,10 @@ function Index({ listing, search, index, pending, filesError }: { listing: Listi
250
251
  <Text component="div" c="dimmed">
251
252
  Everything the project's features say should happen, found by its words. Read what should happen, watch a recording of it, and inspect the record behind it.
252
253
  </Text>
254
+ <Group component="nav" aria-label="Where the evidence is" gap="md" mt="xs">
255
+ <Anchor renderRoot={(props) => <Link {...props} to="/" search={{ tab: 'runs' } as never} />}>Runs</Anchor>
256
+ <Anchor renderRoot={(props) => <Link {...props} to="/" search={{ tab: 'artifacts' } as never} />}>Videos and artifacts</Anchor>
257
+ </Group>
253
258
  </Box>
254
259
  <TextInput
255
260
  type="search"
@@ -273,6 +278,7 @@ function Index({ listing, search, index, pending, filesError }: { listing: Listi
273
278
  </Group>
274
279
  <SearchNotes issues={listing.found.parsed.issues} semantic={listing.found.status} />
275
280
  <CoverageFilter listing={listing} coverage={search.coverage} />
281
+ <ApplicabilityControls state={applicability} stale={index.entries.filter((entry) => needsAttention(entry.assessment)).length} total={index.entries.length} />
276
282
  <Disclosure unread={index.unread} features={index.features} unreadable={index.unreadable} pending={pending} error={filesError} />
277
283
  {selected && (
278
284
  <Group gap="sm" wrap="wrap" role="group" aria-label="Selected behavior">
@@ -316,7 +322,7 @@ function Index({ listing, search, index, pending, filesError }: { listing: Listi
316
322
  {' · '}
317
323
  <Anchor renderRoot={(props) => <Link {...props} to="/specs/$kind" params={{ kind: 'gherkin' }} search={{ spec: selected.file, check: selected.behavior.slot?.id }} />}>Open the feature</Anchor>
318
324
  </Text>
319
- <BehaviorPanel key={selected.id} behavior={selected.behavior} applicability={selected.assessment} inspect={<Inspect entry={selected} />} />
325
+ <BehaviorPanel key={selected.id} behavior={selected.behavior} applicability={selected.assessment} release={applicability.release} inspect={<Inspect entry={selected} />} />
320
326
  </Stack>
321
327
  </Paper>
322
328
  ) : search.behavior ? (
@@ -337,7 +343,7 @@ function Index({ listing, search, index, pending, filesError }: { listing: Listi
337
343
  }
338
344
 
339
345
  /** The dedicated workspace: what should happen beside its recording, the scoped summary, and the record behind a disclosure. */
340
- function Workspace({ entry, listing }: { entry: IndexedBehavior; listing: Listing }) {
346
+ function Workspace({ entry, listing, release }: { entry: IndexedBehavior; listing: Listing; release: ReleaseIdentity }) {
341
347
  const heading = useRef<HTMLHeadingElement>(null);
342
348
  const [inspecting, setInspecting] = useState<string | null>(null);
343
349
  const { behavior } = entry;
@@ -420,7 +426,7 @@ function Workspace({ entry, listing }: { entry: IndexedBehavior; listing: Listin
420
426
  )}
421
427
  {behavior.subject && <Provenance subject={behavior.subject} />}
422
428
  <RevisionLine revision={behavior.revision} />
423
- <ApplicabilitySummary assessment={entry.assessment} release={{}} />
429
+ <ApplicabilitySummary assessment={entry.assessment} release={release} />
424
430
  </Stack>
425
431
  </SimpleGrid>
426
432
  <ScopedSummary behavior={behavior} />
@@ -455,11 +461,12 @@ function WorkspaceMissing({ pending }: { pending: boolean }) {
455
461
  /** The behaviors route: the project-wide index, or the workspace when one behavior is expanded. */
456
462
  export function BehaviorsPage() {
457
463
  const search = useSearch({ from: '/behaviors' });
458
- const { index, pending, filesError } = useBehaviorIndex();
459
- const listing = useListing(index.entries, search);
464
+ const applicability = useApplicabilityState();
465
+ const { index, pending, filesError } = useBehaviorIndex(applicability.release);
466
+ const listing = useListing(index.entries, search, applicability.onlyStale);
460
467
  if (search.workspace) {
461
468
  const entry = index.entries.find((candidate) => candidate.id === search.behavior);
462
- return entry ? <Workspace entry={entry} listing={listing} /> : <WorkspaceMissing pending={pending} />;
469
+ return entry ? <Workspace entry={entry} listing={listing} release={applicability.release} /> : <WorkspaceMissing pending={pending} />;
463
470
  }
464
- return <Index listing={listing} search={search} index={index} pending={pending} filesError={filesError} />;
471
+ return <Index listing={listing} search={search} index={index} pending={pending} filesError={filesError} applicability={applicability} />;
465
472
  }
@@ -73,7 +73,8 @@ function immutable(id: string) {
73
73
  return /^[0-9a-f]{64}$/.test(id);
74
74
  }
75
75
 
76
- function rowsKey(layer: LayerHeader) {
76
+ /** The cache key of one layer's rows, shared by everything that reads them so a layer is read once. */
77
+ export function rowsKey(layer: LayerHeader) {
77
78
  return ['evidence-rows', layer.backend, layer.id, immutable(layer.id) ? '' : layer.execution] as const;
78
79
  }
79
80
 
@@ -264,6 +264,11 @@ export function Explorer() {
264
264
  <Anchor mt="xs" display="inline-block" size="sm" renderRoot={(props) => <Link {...props} to="/" search={patched({ tab: 'health', test: undefined })} resetScroll={false} activeOptions={{ exact: true }} />}>
265
265
  Why do these counts differ from the Tests?
266
266
  </Anchor>
267
+ {kinds.some(({ kind }) => kind.id === 'gherkin') && (
268
+ <Anchor ml="md" mt="xs" display="inline-block" size="sm" renderRoot={(props) => <Link {...props} to="/behaviors" search={{}} />}>
269
+ Behaviors: read what the project promises, then watch it
270
+ </Anchor>
271
+ )}
267
272
  </Box>
268
273
  )}
269
274
  {!model && !evidence.unavailable && (
@@ -23,7 +23,7 @@ import { outcomeHeadline, selectedResult } from '../evidence/outcome';
23
23
  import { useTestHistory } from './catalog';
24
24
  import { releaseOf } from './applicability';
25
25
  import { patched, useExplorerNavigate, useExplorerSearch } from './navigate';
26
- import { Badges, runName } from './parts';
26
+ import { Badges, ReadPromise, runName } from './parts';
27
27
  import { Reliability } from './Reliability';
28
28
  import { ProductLatency } from './ProductLatency';
29
29
  import { usePinnedResult, type PinnedResult } from './pinned';
@@ -237,6 +237,9 @@ export function TestDetail({ test, slot }: { test: TestSummary; slot: CheckSlot
237
237
  </Text>
238
238
  )}
239
239
  {test.spec && <Code c="dimmed">{test.spec!.path}</Code>}
240
+ <Box>
241
+ <ReadPromise specPath={test.spec?.path ?? null} check={test.id} />
242
+ </Box>
240
243
  <Code title="Check id" c="dimmed">
241
244
  {test.id}
242
245
  </Code>
@@ -1,4 +1,4 @@
1
- import { Badge, Flex } from '@mantine/core';
1
+ import { Anchor, Badge, Flex } from '@mantine/core';
2
2
  import { statusColor } from '../ui/theme';
3
3
  /**
4
4
  * @module Explorer parts
@@ -11,6 +11,8 @@ import type { TestSummary } from '@crvouga/atlas/reports/test-catalog';
11
11
  import { plural } from '../evidence/format';
12
12
  import { StatusMark } from '../evidence/marks';
13
13
  import { currentProject } from '../runtime';
14
+ import { kindOfFile, specFile } from '../spec-kind';
15
+ import { Link } from '@tanstack/react-router';
14
16
  import { environmentStatuses } from './groups';
15
17
  /** The project's name for its title: `?project=geviti-app` reads "Geviti App". */
16
18
  export function projectTitle(): string {
@@ -80,3 +82,19 @@ export function EnvironmentChips({ test }: { test: TestSummary }) {
80
82
  </Flex>
81
83
  );
82
84
  }
85
+
86
+ /**
87
+ * "Read the promise": the written spec of a result, opened on that exact check (its scenario and examples) in the spec's own page,
88
+ * where Read, Watch and Inspect sit together. It names the check and no run, so it shows the promise as written now; the run a
89
+ * recording came from stays one click away in the test detail. Nothing is rendered for a result without a spec of a known kind.
90
+ */
91
+ export function ReadPromise({ specPath, check }: { specPath: string | null; check: string }) {
92
+ const file = specPath ? specFile(specPath) : null;
93
+ const kind = file ? kindOfFile(file) : null;
94
+ if (!file || !kind) return null;
95
+ return (
96
+ <Anchor renderRoot={(props) => <Link {...props} to="/specs/$kind" params={{ kind: kind.id }} search={{ spec: file, check }} />}>
97
+ Read the promise
98
+ </Anchor>
99
+ );
100
+ }
@@ -31,6 +31,7 @@ import { MomentBar, MomentPlayback, SeekStep, stepsOfRecording, useIsPlaying, us
31
31
  import { duration, exactTime, plural, statusWord } from '../evidence/format';
32
32
  import { StatusMark } from '../evidence/marks';
33
33
  import { patched } from './navigate';
34
+ import { ReadPromise } from './parts';
34
35
  import { clock, TEXT_KIND_WORD, type RecordingFocus } from './recording-text';
35
36
  import { clipHints, playableVideo, playlistCounts, RECORDING_STATE_TEXT, recordingState, stepFrom, type PlaylistEntry } from './playlist-entries';
36
37
 
@@ -276,13 +277,16 @@ function Clip({
276
277
  };
277
278
 
278
279
  const developer = (
279
- <Anchor
280
- renderRoot={(props) => (
281
- <Link {...props} to="/" search={patched({ tab: undefined, openRun: undefined, artifact: undefined, watch: undefined, watchClip: undefined, test: entry.key, layer: entry.layer })} resetScroll={false} activeOptions={{ exact: true }} onClick={onClose} />
282
- )}
283
- >
284
- Developer details
285
- </Anchor>
280
+ <Group gap="md" wrap="wrap">
281
+ <ReadPromise specPath={entry.specPath} check={entry.check} />
282
+ <Anchor
283
+ renderRoot={(props) => (
284
+ <Link {...props} to="/" search={patched({ tab: undefined, openRun: undefined, artifact: undefined, watch: undefined, watchClip: undefined, test: entry.key, layer: entry.layer })} resetScroll={false} activeOptions={{ exact: true }} onClick={onClose} />
285
+ )}
286
+ >
287
+ Developer details
288
+ </Anchor>
289
+ </Group>
286
290
  );
287
291
 
288
292
  let body;