@crvouga/atlas 1.9.0 → 1.11.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/CHANGELOG.md +15 -1
- package/dist/reports/src/review-collection-store.d.ts +47 -0
- package/dist/reports/src/review-collection-store.js +214 -0
- package/dist/reports/src/review-collection.d.ts +88 -0
- package/dist/reports/src/review-collection.js +144 -0
- package/dist/reports/src/test-catalog.d.ts +2 -0
- package/dist/reports/src/test-catalog.js +1 -1
- package/dist/schema/src/api.d.ts +574 -0
- package/dist/schema/src/api.js +152 -1
- package/dist/server/src/app.js +4 -1
- package/dist/server/src/openapi.js +46 -1
- package/dist/server/src/review-collections.d.ts +39 -0
- package/dist/server/src/review-collections.js +179 -0
- package/docs/extending.md +1 -1
- package/docs/reference/http-api.md +122 -0
- package/docs/reference/reports.md +294 -0
- package/docs/reference/schema.md +131 -0
- package/docs/reference/server.md +44 -0
- package/docs/rich-evidence.md +38 -0
- package/docs/server.md +41 -0
- package/docs/ui.md +67 -1
- package/llms-full.txt +269 -2
- package/package.json +1 -1
- package/packages/schema/schemas/review-collection.schema.json +398 -0
- package/packages/schema/schemas/review-manifest.schema.json +475 -0
- package/packages/server/openapi.json +6729 -4084
- package/packages/visualizer/src/app/router.tsx +6 -1
- package/packages/visualizer/src/evidence/applicability-view.tsx +233 -0
- package/packages/visualizer/src/evidence/applicability.test.ts +131 -0
- package/packages/visualizer/src/evidence/applicability.ts +241 -0
- package/packages/visualizer/src/evidence/artifact-tags.tsx +3 -2
- package/packages/visualizer/src/evidence/behavior-panel.tsx +11 -1
- package/packages/visualizer/src/evidence/feature-page.tsx +25 -4
- package/packages/visualizer/src/explorer/ArtifactsView.tsx +26 -2
- package/packages/visualizer/src/explorer/Explorer.tsx +7 -2
- package/packages/visualizer/src/explorer/RecordingTextSearch.tsx +355 -0
- package/packages/visualizer/src/explorer/RunsView.tsx +11 -1
- package/packages/visualizer/src/explorer/collection-model.test.ts +31 -0
- package/packages/visualizer/src/explorer/collection-model.ts +45 -0
- package/packages/visualizer/src/explorer/collections.tsx +513 -0
- package/packages/visualizer/src/explorer/playlist-entries.test.ts +51 -0
- package/packages/visualizer/src/explorer/playlist-entries.ts +163 -0
- package/packages/visualizer/src/explorer/playlist.tsx +500 -0
- package/packages/visualizer/src/explorer/recording-text.test.ts +149 -0
- package/packages/visualizer/src/explorer/recording-text.ts +490 -0
- package/packages/visualizer/src/explorer/search.ts +10 -2
|
@@ -690,6 +690,128 @@ Scoped to one project. The same project may be selected on the unprefixed API us
|
|
|
690
690
|
|
|
691
691
|
Responses: `201` The recorded entry.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` The token is known but its role may not do this.; `413` Request too large.; `422` Not a tag edit: a bad target, an empty or over-long tag, or nothing to change.
|
|
692
692
|
|
|
693
|
+
### Review collections: curated sets of recordings, newest first
|
|
694
|
+
|
|
695
|
+
`GET /api/reports/review-collections` · role: read
|
|
696
|
+
|
|
697
|
+
The newest revision of every review collection, in brief: id, revision and how many exist, title, purpose, audience, tags, how many entries and gaps it has, who published it and when. A collection is curation kept beside the report; it is not a layer, a check or a run, and it has no status. Open one with `/api/reports/review-collections/detail?id=`. Its share URL is the viewer's `?tab=collections&collection=<id>` and needs the same read access as the rest of the report.
|
|
698
|
+
|
|
699
|
+
- `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
|
|
700
|
+
|
|
701
|
+
Responses: `200` The collections.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.
|
|
702
|
+
|
|
703
|
+
### Review collections: curated sets of recordings, newest first
|
|
704
|
+
|
|
705
|
+
`GET /api/projects/{project}/reports/review-collections` · role: read
|
|
706
|
+
|
|
707
|
+
The newest revision of every review collection, in brief: id, revision and how many exist, title, purpose, audience, tags, how many entries and gaps it has, who published it and when. A collection is curation kept beside the report; it is not a layer, a check or a run, and it has no status. Open one with `/api/reports/review-collections/detail?id=`. Its share URL is the viewer's `?tab=collections&collection=<id>` and needs the same read access as the rest of the report.
|
|
708
|
+
|
|
709
|
+
Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
|
|
710
|
+
|
|
711
|
+
- `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
|
|
712
|
+
|
|
713
|
+
Responses: `200` The collections.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.
|
|
714
|
+
|
|
715
|
+
### Publish an immutable review collection (or a new revision of one)
|
|
716
|
+
|
|
717
|
+
`POST /api/reports/review-collections` · role: publish
|
|
718
|
+
|
|
719
|
+
Freezes an ordered set of references to recorded results as one named collection, recorded under the name of the publish (or admin) token that sends it. Take each entry's `layer` and `check` (and `kind`) from `GET /api/reports/artifacts`, and optionally one of its recordings (`videos[].recording`). The server resolves every entry to the result the report holds and records what the report says about it (status, time, run, commit, environment, tries, videos): the request does not claim them. An entry it cannot resolve refuses the whole request (`422`, naming the entry). The order is yours and is never re-sorted; failed, rejected and incomplete results stay in it beside a later passing replay.
|
|
720
|
+
|
|
721
|
+
Entries from different runs are never one execution: each keeps its own layer, run and status. A collection has no status of its own, adds no check and cannot change any result or winner. A revision is immutable. To change a collection, publish a new revision with `basedOn: { id, revision }` naming the newest revision (`409` otherwise); the earlier ones stay readable. Notes and reviewed marks are separate (`/reviews`). A reader-only token, a browser sign-in box and an anonymous reader are refused; nothing is shared publicly by default.
|
|
722
|
+
|
|
723
|
+
- `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
|
|
724
|
+
|
|
725
|
+
Responses: `201` The recorded revision.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` The token is known but its role may not do this.; `404` `basedOn` names no collection.; `409` `basedOn.revision` is not the newest.; `413` Request too large.; `422` Not a collection, or an entry the report does not hold.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
|
|
726
|
+
|
|
727
|
+
### Publish an immutable review collection (or a new revision of one)
|
|
728
|
+
|
|
729
|
+
`POST /api/projects/{project}/reports/review-collections` · role: publish
|
|
730
|
+
|
|
731
|
+
Freezes an ordered set of references to recorded results as one named collection, recorded under the name of the publish (or admin) token that sends it. Take each entry's `layer` and `check` (and `kind`) from `GET /api/reports/artifacts`, and optionally one of its recordings (`videos[].recording`). The server resolves every entry to the result the report holds and records what the report says about it (status, time, run, commit, environment, tries, videos): the request does not claim them. An entry it cannot resolve refuses the whole request (`422`, naming the entry). The order is yours and is never re-sorted; failed, rejected and incomplete results stay in it beside a later passing replay.
|
|
732
|
+
|
|
733
|
+
Entries from different runs are never one execution: each keeps its own layer, run and status. A collection has no status of its own, adds no check and cannot change any result or winner. A revision is immutable. To change a collection, publish a new revision with `basedOn: { id, revision }` naming the newest revision (`409` otherwise); the earlier ones stay readable. Notes and reviewed marks are separate (`/reviews`). A reader-only token, a browser sign-in box and an anonymous reader are refused; nothing is shared publicly by default.
|
|
734
|
+
|
|
735
|
+
Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
|
|
736
|
+
|
|
737
|
+
- `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
|
|
738
|
+
|
|
739
|
+
Responses: `201` The recorded revision.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` The token is known but its role may not do this.; `404` `basedOn` names no collection.; `409` `basedOn.revision` is not the newest.; `413` Request too large.; `422` Not a collection, or an entry the report does not hold.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
|
|
740
|
+
|
|
741
|
+
### One review collection revision with how its sources stand now
|
|
742
|
+
|
|
743
|
+
`GET /api/reports/review-collections/detail` · role: read
|
|
744
|
+
|
|
745
|
+
The revision (the newest without `revision`), every revision number, the review notes and reviewed marks of that revision, and for each entry `availability`: `available` (its layer is held and the report lists the result), `unverified` (the layer is held but the report no longer lists the result or recording) or `unavailable` (the layer is gone, for example deleted by garbage collection), with the reason. An entry whose source is not available stays in its place with its frozen snapshot; it is never removed or replaced. Entries keep the status each layer recorded, with their own times, in the order the curator chose. A reviewed mark is curation: it changes no outcome and is not an approval.
|
|
746
|
+
|
|
747
|
+
- `id` (query, required): A collection id from the list.
|
|
748
|
+
- `revision` (query): A revision number. Default: the newest.
|
|
749
|
+
- `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
|
|
750
|
+
|
|
751
|
+
Responses: `200` The revision.; `400` No `id`, or a bad `revision`.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `404` No such collection or revision.
|
|
752
|
+
|
|
753
|
+
### One review collection revision with how its sources stand now
|
|
754
|
+
|
|
755
|
+
`GET /api/projects/{project}/reports/review-collections/detail` · role: read
|
|
756
|
+
|
|
757
|
+
The revision (the newest without `revision`), every revision number, the review notes and reviewed marks of that revision, and for each entry `availability`: `available` (its layer is held and the report lists the result), `unverified` (the layer is held but the report no longer lists the result or recording) or `unavailable` (the layer is gone, for example deleted by garbage collection), with the reason. An entry whose source is not available stays in its place with its frozen snapshot; it is never removed or replaced. Entries keep the status each layer recorded, with their own times, in the order the curator chose. A reviewed mark is curation: it changes no outcome and is not an approval.
|
|
758
|
+
|
|
759
|
+
Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
|
|
760
|
+
|
|
761
|
+
- `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
|
|
762
|
+
- `id` (query, required): A collection id from the list.
|
|
763
|
+
- `revision` (query): A revision number. Default: the newest.
|
|
764
|
+
|
|
765
|
+
Responses: `200` The revision.; `400` No `id`, or a bad `revision`.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `404` No such collection or revision.
|
|
766
|
+
|
|
767
|
+
### Add a note, a time-coded gap note or a reviewed mark to a collection entry
|
|
768
|
+
|
|
769
|
+
`POST /api/reports/review-collections/reviews` · role: publish
|
|
770
|
+
|
|
771
|
+
Records one immutable entry, by the name of the publish (or admin) token and the server's time: a `note` or a `gap` (text required; with `entry` and optionally `timecodeMs`, a moment in that entry's recording), or a `reviewed` / `unreviewed` mark for one entry. It changes no check outcome, no layer, no collection revision and no report winner; a mark is not an acceptance sign-off. The notes of a revision are in its `detail`.
|
|
772
|
+
|
|
773
|
+
- `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
|
|
774
|
+
|
|
775
|
+
Responses: `201` The recorded note.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` The token is known but its role may not do this.; `404` No such collection or revision.; `413` Request too large.; `422` Not a note: missing text, an unknown entry, or a mark without an entry.
|
|
776
|
+
|
|
777
|
+
### Add a note, a time-coded gap note or a reviewed mark to a collection entry
|
|
778
|
+
|
|
779
|
+
`POST /api/projects/{project}/reports/review-collections/reviews` · role: publish
|
|
780
|
+
|
|
781
|
+
Records one immutable entry, by the name of the publish (or admin) token and the server's time: a `note` or a `gap` (text required; with `entry` and optionally `timecodeMs`, a moment in that entry's recording), or a `reviewed` / `unreviewed` mark for one entry. It changes no check outcome, no layer, no collection revision and no report winner; a mark is not an acceptance sign-off. The notes of a revision are in its `detail`.
|
|
782
|
+
|
|
783
|
+
Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
|
|
784
|
+
|
|
785
|
+
- `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
|
|
786
|
+
|
|
787
|
+
Responses: `201` The recorded note.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` The token is known but its role may not do this.; `404` No such collection or revision.; `413` Request too large.; `422` Not a note: missing text, an unknown entry, or a mark without an entry.
|
|
788
|
+
|
|
789
|
+
### Portable index (manifest) of one review collection revision
|
|
790
|
+
|
|
791
|
+
`GET /api/reports/review-collections/export` · role: read
|
|
792
|
+
|
|
793
|
+
A manifest an archive can be built from: the collection, each entry with its exact source (layer, check, kind, recording), the status and run it was recorded with, how it stands now, whether it is marked reviewed, and the files it references with their name as recorded, a safe name and path (`files/<position>-<safeName>`), and the recorded `sha256` when the report holds one (`null` is unknown, not verified). A source that cannot be read now is listed under `missing` and its files are marked `missing`; nothing is left out silently. The manifest contains no bytes; recordings are fetched from the attachment routes, which keep their own access control. Sent as an attachment.
|
|
794
|
+
|
|
795
|
+
- `id` (query, required): A collection id.
|
|
796
|
+
- `revision` (query): A revision number. Default: the newest.
|
|
797
|
+
- `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
|
|
798
|
+
|
|
799
|
+
Responses: `200` The manifest.; `400` No `id`, or a bad `revision`.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `404` No such collection or revision.
|
|
800
|
+
|
|
801
|
+
### Portable index (manifest) of one review collection revision
|
|
802
|
+
|
|
803
|
+
`GET /api/projects/{project}/reports/review-collections/export` · role: read
|
|
804
|
+
|
|
805
|
+
A manifest an archive can be built from: the collection, each entry with its exact source (layer, check, kind, recording), the status and run it was recorded with, how it stands now, whether it is marked reviewed, and the files it references with their name as recorded, a safe name and path (`files/<position>-<safeName>`), and the recorded `sha256` when the report holds one (`null` is unknown, not verified). A source that cannot be read now is listed under `missing` and its files are marked `missing`; nothing is left out silently. The manifest contains no bytes; recordings are fetched from the attachment routes, which keep their own access control. Sent as an attachment.
|
|
806
|
+
|
|
807
|
+
Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
|
|
808
|
+
|
|
809
|
+
- `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
|
|
810
|
+
- `id` (query, required): A collection id.
|
|
811
|
+
- `revision` (query): A revision number. Default: the newest.
|
|
812
|
+
|
|
813
|
+
Responses: `200` The manifest.; `400` No `id`, or a bad `revision`.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `404` No such collection or revision.
|
|
814
|
+
|
|
693
815
|
### The runs that recorded the report
|
|
694
816
|
|
|
695
817
|
`GET /api/reports/runs` · role: read
|
|
@@ -7784,6 +7784,298 @@ export async function pruneReports(store: ObjectStore): Promise<ReportRetentionP
|
|
|
7784
7784
|
|
|
7785
7785
|
Same as `planReportRetention`. Despite the name it deletes nothing, and `deleteKeys` is always empty.
|
|
7786
7786
|
|
|
7787
|
+
### Review collections in a store
|
|
7788
|
+
|
|
7789
|
+
Source: `packages/reports/src/review-collection-store.ts`. Import: `@crvouga/atlas/reports/review-collection-store`
|
|
7790
|
+
|
|
7791
|
+
Where review collections are kept: one immutable object per revision under `curation/review-collections/<id>/`, and
|
|
7792
|
+
one small immutable object per reviewer's note under `curation/review-notes/<id>/`, named by time and a random id so
|
|
7793
|
+
editors never overwrite one another. Nothing here is ever changed or deleted by the server, and nothing here touches
|
|
7794
|
+
the layers the entries point at: a collection is curation beside the report, not a layer and not a test run.
|
|
7795
|
+
|
|
7796
|
+
Publishing refuses; it never alters. A request the server cannot resolve to exact, held results is refused with
|
|
7797
|
+
the entry that failed, and what is recorded is what was asked for, plus what the report said about each source.
|
|
7798
|
+
|
|
7799
|
+
See docs/server.md#review-collections
|
|
7800
|
+
|
|
7801
|
+
#### `ReviewCollectionError`
|
|
7802
|
+
|
|
7803
|
+
```ts
|
|
7804
|
+
export class ReviewCollectionError extends Error
|
|
7805
|
+
```
|
|
7806
|
+
|
|
7807
|
+
The request was not acceptable. `status` is the HTTP status to answer: 404 (no such collection), 409 (not the newest revision) or 422.
|
|
7808
|
+
|
|
7809
|
+
#### `ResultFinder`
|
|
7810
|
+
|
|
7811
|
+
```ts
|
|
7812
|
+
export type ResultFinder = (layer: string, check: string, kind: string | undefined) => readonly ArtifactResult[];
|
|
7813
|
+
```
|
|
7814
|
+
|
|
7815
|
+
Finds the recorded results of a layer's check (all kinds when `kind` is undefined).
|
|
7816
|
+
|
|
7817
|
+
#### `reviewRevisions`
|
|
7818
|
+
|
|
7819
|
+
```ts
|
|
7820
|
+
export async function reviewRevisions(store: ObjectStore, id: string): Promise<number[]>
|
|
7821
|
+
```
|
|
7822
|
+
|
|
7823
|
+
The revisions that exist for a collection id, oldest first. Empty when there is none. Transport errors throw.
|
|
7824
|
+
|
|
7825
|
+
#### `readReviewCollection`
|
|
7826
|
+
|
|
7827
|
+
```ts
|
|
7828
|
+
export async function readReviewCollection(store: ObjectStore, id: string, revision?: number): Promise<{ readonly collection: ReviewCollection; readonly revisions: number[] } | null>
|
|
7829
|
+
```
|
|
7830
|
+
|
|
7831
|
+
One revision of a collection (the newest when `revision` is absent), or `null` when there is none or it cannot be read
|
|
7832
|
+
as a collection (unknown, never guessed at). Transport errors throw.
|
|
7833
|
+
|
|
7834
|
+
#### `recordReviewCollection`
|
|
7835
|
+
|
|
7836
|
+
```ts
|
|
7837
|
+
export async function recordReviewCollection(store: ObjectStore, input: unknown, by: string, atMs: number, find: ResultFinder): Promise<ReviewCollection>
|
|
7838
|
+
```
|
|
7839
|
+
|
|
7840
|
+
Validates a request (`ReviewCollectionRequestSchema`) and records it as a new immutable revision, by `by` at `atMs`.
|
|
7841
|
+
Without `basedOn` it is a new collection with a new id; with it, revision `basedOn.revision + 1` of that collection,
|
|
7842
|
+
and `basedOn.revision` must be the newest (409 otherwise) so two curators never silently overwrite each other.
|
|
7843
|
+
|
|
7844
|
+
Every entry is resolved with `find`: it must name a result the report holds (and, when it names a recording, one of
|
|
7845
|
+
that result's recordings) or the whole request is refused with 422 naming the entry. The snapshot recorded for an entry
|
|
7846
|
+
is what the report says about that result, not what the request claims. Entries are kept in the order given and a
|
|
7847
|
+
source may appear once. Throws {@link ReviewCollectionError}; a revision that already exists is never overwritten.
|
|
7848
|
+
|
|
7849
|
+
#### `listReviewCollections`
|
|
7850
|
+
|
|
7851
|
+
```ts
|
|
7852
|
+
export async function listReviewCollections(store: ObjectStore): Promise<{ readonly collection: ReviewCollection; readonly revisions: number }[]>
|
|
7853
|
+
```
|
|
7854
|
+
|
|
7855
|
+
The newest revision of every collection in `store`, newest first. One that cannot be read as a collection is skipped. Transport errors throw.
|
|
7856
|
+
|
|
7857
|
+
#### `recordReviewNote`
|
|
7858
|
+
|
|
7859
|
+
```ts
|
|
7860
|
+
export async function recordReviewNote(store: ObjectStore, input: unknown, by: string, atMs: number): Promise<ReviewNote>
|
|
7861
|
+
```
|
|
7862
|
+
|
|
7863
|
+
Validates a note (`ReviewNoteInputSchema`) against the revision it is about and records it as a new object, by `by` at
|
|
7864
|
+
`atMs`. `note` and `gap` need text; `reviewed` and `unreviewed` need an entry; an entry must be one of the revision's;
|
|
7865
|
+
a timecode needs an entry. Throws {@link ReviewCollectionError} (404 for no such collection or revision). It records
|
|
7866
|
+
curation only: no layer, result or collection revision changes.
|
|
7867
|
+
|
|
7868
|
+
#### `readReviewNotes`
|
|
7869
|
+
|
|
7870
|
+
```ts
|
|
7871
|
+
export async function readReviewNotes(store: ObjectStore, id: string): Promise<ReviewNote[]>
|
|
7872
|
+
```
|
|
7873
|
+
|
|
7874
|
+
Every recorded note of a collection (all revisions), oldest first. One that cannot be read as a note is skipped. Transport errors throw.
|
|
7875
|
+
|
|
7876
|
+
### Review collections
|
|
7877
|
+
|
|
7878
|
+
Source: `packages/reports/src/review-collection.ts`. Import: `@crvouga/atlas/reports/review-collection`
|
|
7879
|
+
|
|
7880
|
+
A review collection is a named, curated, ordered set of references to results that other runs recorded: a few
|
|
7881
|
+
recordings from several executions, a note on what each is for, the gaps nobody recorded yet. It is curation, not a
|
|
7882
|
+
test run. It has no status of its own, carries no check, never decides a result or a winner, and never claims its
|
|
7883
|
+
entries are one execution: each entry keeps the layer, run, revision and status it was recorded with. A revision is
|
|
7884
|
+
immutable; changing a collection is publishing a new revision, explicitly. Reviewers' notes and "reviewed" marks live
|
|
7885
|
+
apart, as an append-only list with who and when, and are not outcomes.
|
|
7886
|
+
|
|
7887
|
+
This module is pure and safe in a browser: how an entry is frozen from the result it points at, how an entry stands
|
|
7888
|
+
now (available, unverified, unavailable), how marks fold, and the portable manifest. The store is
|
|
7889
|
+
`review-collection-store.ts`.
|
|
7890
|
+
|
|
7891
|
+
See docs/server.md#review-collections
|
|
7892
|
+
|
|
7893
|
+
#### `ReviewSource`
|
|
7894
|
+
|
|
7895
|
+
```ts
|
|
7896
|
+
export type ReviewSource = z.infer<typeof ReviewSourceSchema>;
|
|
7897
|
+
```
|
|
7898
|
+
|
|
7899
|
+
The exact result (and optionally one recording of it) an entry points at.
|
|
7900
|
+
|
|
7901
|
+
#### `ReviewSnapshot`
|
|
7902
|
+
|
|
7903
|
+
```ts
|
|
7904
|
+
export type ReviewSnapshot = z.infer<typeof ReviewSnapshotSchema>;
|
|
7905
|
+
```
|
|
7906
|
+
|
|
7907
|
+
What was recorded about a source when it was frozen.
|
|
7908
|
+
|
|
7909
|
+
#### `ReviewEntry`
|
|
7910
|
+
|
|
7911
|
+
```ts
|
|
7912
|
+
export type ReviewEntry = z.infer<typeof ReviewEntrySchema>;
|
|
7913
|
+
```
|
|
7914
|
+
|
|
7915
|
+
One entry of a collection revision.
|
|
7916
|
+
|
|
7917
|
+
#### `ReviewCollection`
|
|
7918
|
+
|
|
7919
|
+
```ts
|
|
7920
|
+
export type ReviewCollection = z.infer<typeof ReviewCollectionSchema>;
|
|
7921
|
+
```
|
|
7922
|
+
|
|
7923
|
+
One immutable revision of a collection.
|
|
7924
|
+
|
|
7925
|
+
#### `ReviewNote`
|
|
7926
|
+
|
|
7927
|
+
```ts
|
|
7928
|
+
export type ReviewNote = z.infer<typeof ReviewNoteSchema>;
|
|
7929
|
+
```
|
|
7930
|
+
|
|
7931
|
+
A recorded note, time-coded gap note or reviewed mark.
|
|
7932
|
+
|
|
7933
|
+
#### `ReviewAvailability`
|
|
7934
|
+
|
|
7935
|
+
```ts
|
|
7936
|
+
export type ReviewAvailability = z.infer<typeof ReviewAvailabilitySchema>;
|
|
7937
|
+
```
|
|
7938
|
+
|
|
7939
|
+
How an entry's source stands now.
|
|
7940
|
+
|
|
7941
|
+
#### `ReviewCollectionDetail`
|
|
7942
|
+
|
|
7943
|
+
```ts
|
|
7944
|
+
export type ReviewCollectionDetail = z.infer<typeof ReviewCollectionDetailSchema>;
|
|
7945
|
+
```
|
|
7946
|
+
|
|
7947
|
+
`GET /api/reports/review-collections/detail`: one revision, how its sources stand now, and its notes.
|
|
7948
|
+
|
|
7949
|
+
#### `ReviewCollectionList`
|
|
7950
|
+
|
|
7951
|
+
```ts
|
|
7952
|
+
export type ReviewCollectionList = z.infer<typeof ReviewCollectionListSchema>;
|
|
7953
|
+
```
|
|
7954
|
+
|
|
7955
|
+
`GET /api/reports/review-collections`: the newest revision of every collection.
|
|
7956
|
+
|
|
7957
|
+
#### `ReviewManifest`
|
|
7958
|
+
|
|
7959
|
+
```ts
|
|
7960
|
+
export type ReviewManifest = z.infer<typeof ReviewManifestSchema>;
|
|
7961
|
+
```
|
|
7962
|
+
|
|
7963
|
+
The portable index of one revision.
|
|
7964
|
+
|
|
7965
|
+
#### `REVIEW_COLLECTIONS_PREFIX`
|
|
7966
|
+
|
|
7967
|
+
```ts
|
|
7968
|
+
export const REVIEW_COLLECTIONS_PREFIX = …
|
|
7969
|
+
```
|
|
7970
|
+
|
|
7971
|
+
Where the revisions of collections are kept in a store: `<prefix><id>/r<revision, six digits>.json`.
|
|
7972
|
+
|
|
7973
|
+
#### `REVIEW_NOTES_PREFIX`
|
|
7974
|
+
|
|
7975
|
+
```ts
|
|
7976
|
+
export const REVIEW_NOTES_PREFIX = …
|
|
7977
|
+
```
|
|
7978
|
+
|
|
7979
|
+
Where reviewers' notes are kept: `<prefix><collection id>/<time>-<random>.json`, one object each.
|
|
7980
|
+
|
|
7981
|
+
#### `REVIEW_ID`
|
|
7982
|
+
|
|
7983
|
+
```ts
|
|
7984
|
+
export const REVIEW_ID = …
|
|
7985
|
+
```
|
|
7986
|
+
|
|
7987
|
+
A collection id as the server mints it and the address carries it. Anything else is not an id (and never reaches a store key).
|
|
7988
|
+
|
|
7989
|
+
#### `reviewCollectionKey`
|
|
7990
|
+
|
|
7991
|
+
```ts
|
|
7992
|
+
export function reviewCollectionKey(id: string, revision: number): string
|
|
7993
|
+
```
|
|
7994
|
+
|
|
7995
|
+
The store key of one revision of a collection.
|
|
7996
|
+
|
|
7997
|
+
#### `sameSource`
|
|
7998
|
+
|
|
7999
|
+
```ts
|
|
8000
|
+
export function sameSource(a: ReviewSource, b: ReviewSource): boolean
|
|
8001
|
+
```
|
|
8002
|
+
|
|
8003
|
+
Whether two sources name the same result and recording. `kind` counts only when both have one.
|
|
8004
|
+
|
|
8005
|
+
#### `snapshotOf`
|
|
8006
|
+
|
|
8007
|
+
```ts
|
|
8008
|
+
export function snapshotOf(result: ArtifactResult): ReviewSnapshot
|
|
8009
|
+
```
|
|
8010
|
+
|
|
8011
|
+
What is recorded about a result when an entry is frozen: its own status, time, run, commit and videos, as the layer
|
|
8012
|
+
recorded them. Nothing here is the collection's opinion, and none of it is ever updated: a later run does not change it.
|
|
8013
|
+
|
|
8014
|
+
#### `availabilityOf`
|
|
8015
|
+
|
|
8016
|
+
```ts
|
|
8017
|
+
export function availabilityOf(
|
|
8018
|
+
collection: Pick<ReviewCollection, 'entries'>,
|
|
8019
|
+
layerHeld: (layer: string) => boolean,
|
|
8020
|
+
results: (layer: string, check: string, kind: string | undefined) => readonly ArtifactResult[]
|
|
8021
|
+
): ReviewAvailability[]
|
|
8022
|
+
```
|
|
8023
|
+
|
|
8024
|
+
How the sources of a collection stand now. `layerHeld` says whether a layer is still in a backend; `results` finds the
|
|
8025
|
+
results the report holds. A layer that is gone makes its entries `unavailable`; a layer that is held while the report
|
|
8026
|
+
no longer lists the result (or the recording) is `unverified`, not available and not gone. Every entry keeps its place
|
|
8027
|
+
and its explanation: nothing is removed or replaced.
|
|
8028
|
+
|
|
8029
|
+
#### `foldReviewed`
|
|
8030
|
+
|
|
8031
|
+
```ts
|
|
8032
|
+
export function foldReviewed(notes: readonly ReviewNote[]): Record<string, { readonly by: string; readonly atMs: number }>
|
|
8033
|
+
```
|
|
8034
|
+
|
|
8035
|
+
Folds the notes into the reviewed marks that stand now: for each entry the newest `reviewed` or `unreviewed` decides
|
|
8036
|
+
(ties in time go to the later id). Only entries currently `reviewed` are returned, with who and when. A mark is
|
|
8037
|
+
curation: it is not a check outcome and does not mean anyone accepted the behavior.
|
|
8038
|
+
|
|
8039
|
+
#### `safeFileName`
|
|
8040
|
+
|
|
8041
|
+
```ts
|
|
8042
|
+
export function safeFileName(name: string): string
|
|
8043
|
+
```
|
|
8044
|
+
|
|
8045
|
+
A name that is safe to write to any file system: the last path segment only, letters, digits, dot, dash and
|
|
8046
|
+
underscore (everything else becomes `_`), no leading dot, at most 100 characters, never empty.
|
|
8047
|
+
|
|
8048
|
+
**Example**
|
|
8049
|
+
|
|
8050
|
+
```ts
|
|
8051
|
+
safeFileName('../../etc/pass wd.webm'); // 'pass_wd.webm'
|
|
8052
|
+
```
|
|
8053
|
+
|
|
8054
|
+
#### `digestOf`
|
|
8055
|
+
|
|
8056
|
+
```ts
|
|
8057
|
+
export function digestOf(recording: string | undefined): string | null
|
|
8058
|
+
```
|
|
8059
|
+
|
|
8060
|
+
The digest in a recording identity (`sha256:<64 hex>`), or null when the identity is a bundle key or address: the bytes' hash is then not known.
|
|
8061
|
+
|
|
8062
|
+
#### `manifestOf`
|
|
8063
|
+
|
|
8064
|
+
```ts
|
|
8065
|
+
export function manifestOf(
|
|
8066
|
+
collection: ReviewCollection,
|
|
8067
|
+
availability: readonly ReviewAvailability[],
|
|
8068
|
+
reviewed: Readonly<Record<string, unknown>>,
|
|
8069
|
+
generatedAtMs: number
|
|
8070
|
+
): ReviewManifest
|
|
8071
|
+
```
|
|
8072
|
+
|
|
8073
|
+
The portable index of one revision: every entry with where its result came from, how it stands now, whether it was
|
|
8074
|
+
marked reviewed, and the files (recordings) it references, each with its recorded digest when there is one and a name
|
|
8075
|
+
that is safe to write. A file whose source cannot be read now is listed as `missing`, with the reason under `missing`
|
|
8076
|
+
of the manifest, never left out. The manifest has no bytes; it is what a packer needs to build an archive that says
|
|
8077
|
+
what it lacks.
|
|
8078
|
+
|
|
7787
8079
|
### Run bundles
|
|
7788
8080
|
|
|
7789
8081
|
Source: `packages/reports/src/run-bundle.ts`. Import: `@crvouga/atlas/reports/run-bundle`
|
|
@@ -9391,6 +9683,8 @@ export type ArtifactResult = {
|
|
|
9391
9683
|
readonly types: readonly string[];
|
|
9392
9684
|
readonly labels: Readonly<Record<string, string>>;
|
|
9393
9685
|
readonly videos?: readonly VideoRef[];
|
|
9686
|
+
/** The status of each recorded try in order, when the check was retried. Absent when no try was recorded: a first try is then unknown, never assumed to have passed. */
|
|
9687
|
+
readonly attemptStatuses?: readonly EvidenceStatus[];
|
|
9394
9688
|
};
|
|
9395
9689
|
```
|
|
9396
9690
|
|
package/docs/reference/schema.md
CHANGED
|
@@ -233,6 +233,137 @@ export const ArtifactTagsSchema = …
|
|
|
233
233
|
|
|
234
234
|
`GET /api/reports/artifact-tags`: every recorded edit, oldest first. Fold them with `foldArtifactTags`.
|
|
235
235
|
|
|
236
|
+
#### `ReviewSourceSchema`
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
export const ReviewSourceSchema = …
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
A source a review collection points at: one recorded result of one archived layer, and optionally one of its recordings. Exact and immutable: a layer never changes.
|
|
243
|
+
|
|
244
|
+
#### `ReviewEntryInputSchema`
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
export const ReviewEntryInputSchema = …
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
One entry of a request to publish a collection: a source and the curator\'s caption for it.
|
|
251
|
+
|
|
252
|
+
#### `ReviewGapInputSchema`
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
export const ReviewGapInputSchema = …
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
A gap the curator names: something the review should show and no recording does. It sits after the first `after` entries (0 or absent: before the first).
|
|
259
|
+
|
|
260
|
+
#### `ReviewCollectionRequestSchema`
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
export const ReviewCollectionRequestSchema = …
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
The body of `POST /api/reports/review-collections`: a new collection, or, with `basedOn`, an explicit new revision of
|
|
267
|
+
one. At least one entry or gap. The server records which sources the entries are now; it never alters the request.
|
|
268
|
+
|
|
269
|
+
#### `ReviewSnapshotSchema`
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
export const ReviewSnapshotSchema = …
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
What the server recorded about the source of an entry at the moment it was frozen.
|
|
276
|
+
|
|
277
|
+
#### `ReviewEntrySchema`
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
export const ReviewEntrySchema = …
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
One entry of a frozen collection.
|
|
284
|
+
|
|
285
|
+
#### `ReviewGapSchema`
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
export const ReviewGapSchema = …
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
A gap item of a collection.
|
|
292
|
+
|
|
293
|
+
#### `ReviewCollectionSchema`
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
export const ReviewCollectionSchema = …
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
One revision of a review collection, as stored: immutable. It is curation, not a test run: it has no status of its own,
|
|
300
|
+
carries no check, and its entries are references to results that other runs recorded. Entries from different
|
|
301
|
+
runs are never one execution.
|
|
302
|
+
|
|
303
|
+
#### `ReviewCollectionSummarySchema`
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
export const ReviewCollectionSummarySchema = …
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
One collection in the list: its newest revision in brief.
|
|
310
|
+
|
|
311
|
+
#### `ReviewCollectionListSchema`
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
export const ReviewCollectionListSchema = …
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
`GET /api/reports/review-collections`: the newest revision of every collection, newest first.
|
|
318
|
+
|
|
319
|
+
#### `ReviewNoteInputSchema`
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
export const ReviewNoteInputSchema = …
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
The body of `POST /api/reports/review-collections/reviews`: a note, a time-coded gap note, or a reviewed marker on one entry. Curation only: it changes no outcome.
|
|
326
|
+
|
|
327
|
+
#### `ReviewNoteSchema`
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
export const ReviewNoteSchema = …
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
One recorded review note. Entries are never changed or removed: they are the audit trail.
|
|
334
|
+
|
|
335
|
+
#### `ReviewAvailabilitySchema`
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
export const ReviewAvailabilitySchema = …
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
How an entry\'s source stands now: `available` (the layer and the result are in the report), `unverified` (the layer is held but the result is outside the report\'s history window) or `unavailable` (the layer is gone).
|
|
342
|
+
|
|
343
|
+
#### `ReviewCollectionDetailSchema`
|
|
344
|
+
|
|
345
|
+
```ts
|
|
346
|
+
export const ReviewCollectionDetailSchema = …
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
`GET /api/reports/review-collections/detail`: one revision, how each source stands now, and the review notes.
|
|
350
|
+
|
|
351
|
+
#### `ReviewManifestFileSchema`
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
export const ReviewManifestFileSchema = …
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
One file in an export manifest.
|
|
358
|
+
|
|
359
|
+
#### `ReviewManifestSchema`
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
export const ReviewManifestSchema = …
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
`GET /api/reports/review-collections/export`: a portable index of one revision. It lists sources and files; it contains no bytes.
|
|
366
|
+
|
|
236
367
|
#### `HelloEventSchema`
|
|
237
368
|
|
|
238
369
|
```ts
|
package/docs/reference/server.md
CHANGED
|
@@ -625,6 +625,50 @@ export function projectServer(options: AtlasServerOptions, create: (options: Atl
|
|
|
625
625
|
|
|
626
626
|
Wrap the single-project protocol in project selection. Auth, upload validation and composition stay identical.
|
|
627
627
|
|
|
628
|
+
### Review collection routes
|
|
629
|
+
|
|
630
|
+
Source: `packages/server/src/review-collections.ts`
|
|
631
|
+
|
|
632
|
+
`/api/reports/review-collections`: publish an immutable, named, ordered collection of references to recorded results
|
|
633
|
+
(with notes and gaps), read it back with how every source stands now, add reviewers' notes and "reviewed" marks, and
|
|
634
|
+
export a portable manifest. A collection is curation kept beside the report under `curation/`: it is never a layer, a
|
|
635
|
+
check or a run, changes no status, slot or winner, and never alters what is uploaded. Reading needs a reader (the
|
|
636
|
+
same as the rest of the report, so nothing becomes public by sharing a link); publishing and noting need a Bearer
|
|
637
|
+
token with the `publish` role, recorded under that token's name. Recordings are still served only by the attachment
|
|
638
|
+
routes, under their own access control.
|
|
639
|
+
|
|
640
|
+
See docs/server.md#review-collections
|
|
641
|
+
|
|
642
|
+
#### `ReviewCollectionDeps`
|
|
643
|
+
|
|
644
|
+
```ts
|
|
645
|
+
export type ReviewCollectionDeps = {
|
|
646
|
+
/** Where collections and notes are kept: the write backend's store. */
|
|
647
|
+
readonly store: ObjectStore;
|
|
648
|
+
/** Every backend's store, to tell whether a layer is still held anywhere. */
|
|
649
|
+
readonly stores: readonly ObjectStore[];
|
|
650
|
+
readonly auth: Auth;
|
|
651
|
+
readonly index: ReportIndex;
|
|
652
|
+
readonly now: () => number;
|
|
653
|
+
/** The largest request body, in bytes. */
|
|
654
|
+
readonly maxBytes: number;
|
|
655
|
+
readonly unauthorized: (verdict: Extract<ReturnType<Auth['check']>, { ok: false }>) => Response;
|
|
656
|
+
};
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
What the review collection routes need from the server around them.
|
|
660
|
+
|
|
661
|
+
#### `mountReviewCollections`
|
|
662
|
+
|
|
663
|
+
```ts
|
|
664
|
+
export function mountReviewCollections(app: Hono, deps: ReviewCollectionDeps): void
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
Registers the review collection routes on `app`:
|
|
668
|
+
`GET /api/reports/review-collections`, `POST /api/reports/review-collections`,
|
|
669
|
+
`GET /api/reports/review-collections/detail`, `POST /api/reports/review-collections/reviews` and
|
|
670
|
+
`GET /api/reports/review-collections/export`.
|
|
671
|
+
|
|
628
672
|
### settings-page.ts
|
|
629
673
|
|
|
630
674
|
Source: `packages/server/src/settings-page.ts`
|
package/docs/rich-evidence.md
CHANGED
|
@@ -183,6 +183,44 @@ curl "$ATLAS_SERVER_URL/api/reports/artifacts?artifactTag=demo&type=video&domain
|
|
|
183
183
|
**live query**: opening it asks again and shows what matches the newest report. It is not a snapshot; an immutable
|
|
184
184
|
review collection is a separate feature. *Copy link* gives the address, which holds the whole query.
|
|
185
185
|
|
|
186
|
+
## Words in recordings
|
|
187
|
+
|
|
188
|
+
A reviewer who remembers a phrase that was spoken or shown in a video can search for it on the **Artifacts** tab
|
|
189
|
+
(**Search inside recordings**, the `inside` address parameter). Atlas never transcribes video or reads a screen: it
|
|
190
|
+
searches text you attach, and nothing is read from a recording until words are typed. Attach it beside the video, at the
|
|
191
|
+
same level (the check, or the retry attempt that recorded the video):
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
attachments: [
|
|
195
|
+
{ id: 'video', name: 'checkout.webm', contentType: 'video/webm', data: video },
|
|
196
|
+
// Spoken words: WebVTT, SubRip, JSON segments or plain text.
|
|
197
|
+
{ name: 'checkout.vtt', role: 'transcript', contentType: 'text/vtt', data: transcript,
|
|
198
|
+
extensions: { 'atlas.text': { recording: 'video', generated: true, generator: 'my-speech-tool' } } },
|
|
199
|
+
// Text shown on screen, with its own timing.
|
|
200
|
+
{ name: 'screen.json', role: 'ocr', contentType: 'application/json', data: JSON.stringify({ segments: [{ start: 2, end: 4, text: 'Order total', confidence: 0.9 }] }) }
|
|
201
|
+
]
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
- **Which attachment is text.** One with `extensions["atlas.text"]`, a `role` of `transcript`, `ocr`, `screen-text`,
|
|
205
|
+
`captions` or `subtitles`, or a WebVTT or SubRip type. `extensions["atlas.text"].kind` (`speech`, `screen`, `caption`)
|
|
206
|
+
says what it is; otherwise the role does. It belongs to the video that lists its `id` in `captions`, or that
|
|
207
|
+
`extensions["atlas.text"].recording` names by `id`, else to the only video at the same level.
|
|
208
|
+
- **Segments.** WebVTT and SubRip cues; JSON as `{ "segments": [...] }` (or `cues`, `results`, `lines`, or a bare list) of
|
|
209
|
+
`{ text, start, end }` in seconds or `{ text, startMs, endMs }`, with an optional `confidence` between 0 and 1. A
|
|
210
|
+
segment whose time is missing or invalid is still searched and is shown with no time; the viewer offers the recording
|
|
211
|
+
and says seeking is unavailable, and never guesses a moment.
|
|
212
|
+
- **Honesty about the text.** `generated: true` (with `generator`) marks text a tool produced; results show it with its
|
|
213
|
+
confidence and say it may contain errors. A result always shows the indexed words, never a reworded quotation. A
|
|
214
|
+
match finds a moment; it is not a new assertion, and the result's own outcome and the retry attempt stay beside it.
|
|
215
|
+
- **States are different things.** Searched with no match; not indexed (no text was supplied); unsupported (the video is
|
|
216
|
+
silent by its `scope.audioCaptured: false`, or the text is in a form that is not read, or sits at a `uri`); failed
|
|
217
|
+
(`extensions["atlas.text"].state: "failed"` with a `reason`, a file left out with `omitted`, or text that did not
|
|
218
|
+
parse). The ones not searched are listed apart and never counted as "no match".
|
|
219
|
+
- **Access.** The text is read through the same report API, and so the same access rules, that play the recording. A file
|
|
220
|
+
the viewer may not read is not indexed and appears in no snippet or count. The index exists only in the open page: there
|
|
221
|
+
is no stored or cross-project index. Sending evidence to a transcription or OCR service is the producer's explicit
|
|
222
|
+
decision and happens outside Atlas.
|
|
223
|
+
|
|
186
224
|
## Labels describe, parameters split
|
|
187
225
|
|
|
188
226
|
A result's **slot** is its producer, check id, part kind, spec, environment and **parameters**. The newest completed
|