# Findings

## Auth

getting a token, and what it is

- [001 auth](https://sg-groundtruth.vercel.app/findings/001_auth.md): Send the token request as `application/x-www-form-urlencoded`: `application/json` is 400 Invalid JSON body. client_credentials returns a 600s bearer, so ignore the refresh_token and re-auth.
- [027 auth permissions](https://sg-groundtruth.vercel.app/findings/027_auth_permissions.md): The token endpoint accepts password and session_token. Impersonation is the OAuth2 scope sudo_as_login:<login>, never a body field, and a lower level reads far fewer rows and as many fields.
- [052 app session launcher](https://sg-groundtruth.vercel.app/findings/052_app_session_launcher.md): Post appName and machineId, open url in a browser, PUT the id until approved. The sessionToken spends at grant_type=session_token as that person, and every mint renews the session. (partly measured: a person clicking deny, and a session left idle past the site's expiry window; both wait on time and on the site, not on the probe)

## Protocol

headers, and what a status code is worth

- [004 array vs hash](https://sg-groundtruth.vercel.app/findings/004_array_vs_hash.md): api3_array/api3_hash are a POST _search request Content-Type, not a GET Accept header: as Accept they 406, and entity fields are returned under relationships either way.
- [028 loud and silent](https://sg-groundtruth.vercel.app/findings/028_loud_and_silent.md): A 400 is trustworthy and usually names the legal set, but a 200 proves nothing: an unknown field, sort key or query param is a no-op, and a batch can return an id for a row it never made.
- [051 api version](https://sg-groundtruth.vercel.app/findings/051_api_version.md): /api/v1 and /api/v1.1 are the same API. Across 20 read-only calls the only difference is api_version in the root document and the prefix each echoes in its own links. Any other segment is 404.
- [062 cors](https://sg-groundtruth.vercel.app/findings/062_cors.md): Every path under `/api/v1` answers the preflight and echoes any `Origin`, credentials true. `/internal_api` and the web paths send no CORS header, so a page on another origin proxies those. (partly measured: what a live session cookie authenticates on /api/v1, since allow-credentials invites one, and the presigned upload host, which is not this site. A live session needs a person at a browser)

## Schema

what the site has, and adding to it

- [002 schema](https://sg-groundtruth.vercel.app/findings/002_schema.md): Fetch /schema once for the type list, then /schema/<Type>/fields only for types you actually need: it is the expensive call (48KB, ~330ms each) and must never be looped over all types.
- [008 custom entities](https://sg-groundtruth.vercel.app/findings/008_custom_entities.md): Presence in /schema is the enablement test for a custom entity: a slot absent from the listing 404s. Slot numbers are non-contiguous and site-specific, so read name.value and never hardcode one.
- [009 status lists](https://sg-groundtruth.vercel.app/findings/009_status_lists.md): A project's usable statuses are valid_values minus hidden_values, read with project_id: valid_values is identical at every scope, hidden_values is the only thing that varies.
- [019 create fields](https://sg-groundtruth.vercel.app/findings/019_create_fields.md): Custom fields are creatable over REST, but you pass a display name and a duplicate silently becomes <name>_1: an idempotent ensure() must read /schema first, never POST-and-hope.
- [040 field revive](https://sg-groundtruth.vercel.app/findings/040_field_revive.md): A trashed field is revived by POST /schema/<Type>/fields/<name> with {"revive": true} at 204, but it returns at its original data_type, and a PUT changing data_type is a 200 that does nothing.
- [042 spec coverage](https://sg-groundtruth.vercel.app/findings/042_spec_coverage.md): `GET /spec.json` returns the deployment's own OpenAPI v3 document. It advertises 62 operations against the 23 this corpus covers, and it disagrees with the published documentation.
- [047 site facts and the working week](https://sg-groundtruth.vercel.app/findings/047_site_facts_and_the_working_week.md): Three site-fact calls, three different envelopes: `{data, status}`, a bare hash, and JSON:API. Only `/schedule/work_day_rules` reports a bad scope id, and it reports it as a 200.
- [056 stock vs custom field](https://sg-groundtruth.vercel.app/findings/056_stock_vs_custom_field.md): A field with `visible.editable` false is stock and safe to depend on; true means the site can hide it, which is every custom field and a few stock ones. The `sg_` prefix decides nothing.
- [061 shipped statuses](https://sg-groundtruth.vercel.app/findings/061_shipped_statuses.md): Nothing in the schema marks a shipped Status. `system` is true on a minority of them; the stock set is `created_by is null`, plus `options[return_only]=retired` for the rows a site retired.

## Read

getting rows back

- [003 query](https://sg-groundtruth.vercel.app/findings/003_query.md): A dotted ?fields path comes back flat under literal key "sg_task.Task.content" in attributes; an entity field is returned under relationships as {data, links}. Never read a row from attributes alone.
- [006 pagination](https://sg-groundtruth.vercel.app/findings/006_pagination.md): links.next is emitted on every page forever, including zero-row ones, so stop paging when data is empty and never on a missing next.
- [018 project listing](https://sg-groundtruth.vercel.app/findings/018_project_listing.md): sg_status is not a liveness filter and is null on 15 of 22 projects; is_template, is_demo and archived are the discriminators, so pick the ones your list wants - is_demo hides the demo show.
- [021 media resolution](https://sg-groundtruth.vercel.app/findings/021_media_resolution.md): PublishedFile.path is returned with the LocalStorage join already done, so a client never reads LocalStorage or reassembles a root, but a platform whose storage root is unset reads null.
- [023 pages](https://sg-groundtruth.vercel.app/findings/023_pages.md): A page's layout is the PageSetting row whose user is null; settings_json reads back as decoded JSON and body/list_content settings.columns is the column list. Every filter on it is ignored.
- [026 result order](https://sg-groundtruth.vercel.app/findings/026_result_order.md): Rows come back id ascending unless you sort; ["id", "in", [...]] discards the order of the list, and an unsortable or unknown sort field is a silent 200 no-op where the same name in a filter 400s.
- [048 one record beyond crud](https://sg-groundtruth.vercel.app/findings/048_one_record_beyond_crud.md): POST on one record is revive, not update: `?revive=1` is required and the body is ignored. `/<field>` reads image and attachment fields only, and `relationships/<field>` is the same data, unpaged.
- [059 dotted path type check](https://sg-groundtruth.vercel.app/findings/059_dotted_path_type_check.md): The middle segment of a dotted path is checked against the field's valid_types in a projection and against the schema alone in a filter: the projection drops the key at 200, the filter 400s. (partly measured: whether a filter through a type outside valid_types is evaluated or matches nothing; no row on a read-only project links one, so it needs a write)
- [060 entity dict name](https://sg-groundtruth.vercel.app/findings/060_entity_dict_name.md): The `name` in an entity dict is the target's `cached_display_name`, filled on every type measured, single and multi alike. Read it, not the per-type identity field, and expect decoration.
- [064 hierarchy expand buckets](https://sg-groundtruth.vercel.app/findings/064_hierarchy_expand_buckets.md): Dedupe `children` by `path` and keep the first. The `__none__` bucket is repeated once per group, byte-identical every time, and its rows are disjoint from every group's. (partly measured: how the web interface draws the repeated node. Its tree needs a session a person approves (probe 052), and the saved token had expired)

## Filter

selecting the rows you want

- [016 dotted multi entity](https://sg-groundtruth.vercel.app/findings/016_dotted_multi_entity.md): A dotted path through a multi_entity field reads back nothing: HTTP 200 with the key silently absent from attributes. Filters on that same path work, including two hops.
- [017 filter operators](https://sg-groundtruth.vercel.app/findings/017_filter_operators.md): is/is_not/contains/not_contains/starts_with/ends_with/in/not_in all work, on text fields and through dotted paths; an unknown operator 400s on all 21 data types, naming the valid list on 16.
- [020 summarize](https://sg-groundtruth.vercel.app/findings/020_summarize.md): _summarize needs the same vendor Content-Type as _search, and one `grouping` call returns a field's distinct-value count and its empty count. At ~300ms a field, rank a shortlist, never scan.
- [030 complex filters](https://sg-groundtruth.vercel.app/findings/030_complex_filters.md): api3_hash nests and/or groups 265 deep and mixes leaves with sub-groups; api3_array cannot express or, query-string filter[] is ignored on _search, and {path,relation,values} runs nowhere.
- [046 search without a path](https://sg-groundtruth.vercel.app/findings/046_search_without_a_path.md): `/hierarchy/_expand` and `/hierarchy/_search` refuse the vendor content types every other POST requires and take `application/json` alone, so one shared POST helper 415s on half the API.
- [053 text search matching](https://sg-groundtruth.vercel.app/findings/053_text_search_matching.md): `page.size` caps at 25 and defaults to 25 with no `links`, so page with `page.number`. Every word must match a case-insensitive substring of the name or of the linked row's name. (partly measured: Only `description` and a linked row's name were tried as fields beyond the name, and the tie-break between equal-length names was not reached. The probe's limit, not the site's.)
- [063 text search filter shape](https://sg-groundtruth.vercel.app/findings/063_text_search_filter_shape.md): An `entity_types` value follows the request Content-Type: an array of triples under api3_array, a `logical_operator` group under api3_hash, which alone nests. The other shape is 400 code 103.
- [068 note read state](https://sg-groundtruth.vercel.app/findings/068_note_read_state.md): read_by_current_user is per person and missing from the schema; `is` and `is_not` are evaluated, while `in`, `not_in` and an unknown `is` value all return the unread rows at 200.
- [071 note link name filter](https://sg-groundtruth.vercel.app/findings/071_note_link_name_filter.md): Filter notes about a thing on `note_links.<Type>.cached_display_name`: it resolves for every valid type, `code` 400s on Booking and `name` on all but Department. The path cannot be read back.

## Write

creating and updating

- [011 create project](https://sg-groundtruth.vercel.app/findings/011_create_project.md): A script user can create a Project with nothing but {"name": ...}, at 201, but the response echoes only 6 attributes, so read the project back if you need anything else.
- [012 create version](https://sg-groundtruth.vercel.app/findings/012_create_version.md): The schema's mandatory flags are not the create contract: on every project-scoped type measured, `project` is required and the identity field is optional, server-generated and not unique.
- [024 read after write](https://sg-groundtruth.vercel.app/findings/024_read_after_write.md): Every write ignores ?fields. A create returns what you sent plus the server defaults, an update returns the whole record, and neither resolves a dotted path, so re-read for those and after an upload.
- [045 webhooks](https://sg-groundtruth.vercel.app/findings/045_webhooks.md): The hook contract validates the url and the entity type, and silently accepts a field name, a project id and an entity-type count it will never honour. test_connection answers 204 for any uuid. (partly measured: No entity event reached a hook on the probed site, so the delivery payload, the `X-SG-SIGNATURE` and `x-sg-event-batch-*` headers, and `batch_deliveries`, are all unrecorded.)
- [050 webhook subscriptions](https://sg-groundtruth.vercel.app/findings/050_webhook_subscriptions.md): entity_types and event_type are mutually exclusive and one 400 covers giving neither and giving both. revive is a fourth action, and every entity the guide calls excluded is accepted at 201.
- [058 local storage roots](https://sg-groundtruth.vercel.app/findings/058_local_storage_roots.md): One create fills every `local_path_*` the storage row defines, whichever platform's root the path was under. The server picks the deepest matching root, and no conditional-write header is honoured.
- [069 client note](https://sg-groundtruth.vercel.app/findings/069_client_note.md): `client_note` cannot be set over REST: `true` on create is 400 and any `PUT` is 400 `editable on create only`. `sg_note_type: "Client"` is the one marker a caller can write.
- [070 authored timestamps](https://sg-groundtruth.vercel.app/findings/070_authored_timestamps.md): A create body sets created_at and updated_at and they read back exactly, on Note, Task and Version, though the schema flags both editable false; every PUT on either 400s. (partly measured: Whether an EventLogEntry create can date itself is untried: that row cannot be deleted afterwards (probe 025), so the probe did not spend one.)

## Upload

getting bytes in and out

- [013 upload media](https://sg-groundtruth.vercel.app/findings/013_upload_media.md): Media upload is three calls: GET `{field}/_upload`, PUT the bytes, POST `links.complete_upload`. Transcoding is async: poll until the field stops reading `/images/status/transient/`.
- [014 attach file](https://sg-groundtruth.vercel.app/findings/014_attach_file.md): Leave the field out of the _upload path and the file is stored as an Attachment on attachment_links; read it back with POST /entity/attachments/_search, never flat filter[].
- [022 sequence on version](https://sg-groundtruth.vercel.app/findings/022_sequence_on_version.md): sg_uploaded_movie is single-valued, and replacing it leaves sg_uploaded_movie_mp4 describing the old file while status reads 1. A sequence belongs in sg_path_to_frames.
- [039 upload silent failures](https://sg-groundtruth.vercel.app/findings/039_upload_silent_failures.md): complete_upload returns 201 and creates an Attachment even when the bytes were never PUT, and file_size is null on a good upload too, so only fetching the stored file proves it exists.
- [044 multipart upload](https://sg-groundtruth.vercel.app/findings/044_multipart_upload.md): `multipart_upload=true` on the init sets `upload_id` and adds `links.get_next_part`. Every part but the last must be at least 5 MiB, and completion needs an `etags` array inside `upload_info`.

## Observe

what changed

- [025 event log](https://sg-groundtruth.vercel.app/findings/025_event_log.md): meta.old_value and meta.new_value answer "what was this before", but meta is unfilterable and unsortable: narrow on entity, event_type and attribute_name, sort -id, read meta yourself. (partly measured: Whether event_type or meta can be set in the create body is untried: testing it costs another permanent row. Every event read was generated by another user; probe 049 covers the script's own.)
- [043 attention](https://sg-groundtruth.vercel.app/findings/043_attention.md): The six attention calls share no convention with the rest of the API: no paging, no `fields`, `links.self` spelled `/entity/Shot/7668`, and a missing record id on activity_stream is a 500.
- [049 script events](https://sg-groundtruth.vercel.app/findings/049_script_events.md): A script's writes reach the event log only while its ApiUser has generate_event_log_entries True. The default is False and nothing errors when off. One create logs one row per field plus one _New.
- [066 user feed](https://sg-groundtruth.vercel.app/findings/066_user_feed.md): A HumanUser's activity_stream is what the person created, not what they follow: 0 of 9 rows touched the 81 followed records. A feed is a fan-out over their tasks' Shots and Assets.
- [067 notes in the stream](https://sg-groundtruth.vercel.app/findings/067_notes_in_the_stream.md): A Reply reaches every linked stream in 33 s as `create_reply`, creates too; a script's Note create and status changes were absent after 430 s. Write as a person.

## Render

showing it to a person

- [010 status icons](https://sg-groundtruth.vercel.app/findings/010_status_icons.md): Status.icon is an entity link under relationships; display_type picks one of three renderings; url is empty unless image_data is asked for beside it; the stock sprite is in the site's own stylesheet.

The rules these entries teach, copied whole: [doors/findings-auth.md](https://sg-groundtruth.vercel.app/doors/findings-auth.md), [doors/findings-protocol.md](https://sg-groundtruth.vercel.app/doors/findings-protocol.md), [doors/findings-schema.md](https://sg-groundtruth.vercel.app/doors/findings-schema.md), [doors/findings-read.md](https://sg-groundtruth.vercel.app/doors/findings-read.md), [doors/findings-filter.md](https://sg-groundtruth.vercel.app/doors/findings-filter.md), [doors/findings-write.md](https://sg-groundtruth.vercel.app/doors/findings-write.md), [doors/findings-upload.md](https://sg-groundtruth.vercel.app/doors/findings-upload.md), [doors/findings-observe.md](https://sg-groundtruth.vercel.app/doors/findings-observe.md), [doors/findings-render.md](https://sg-groundtruth.vercel.app/doors/findings-render.md).

Append `.md` to any entry URL for the file that page was built from.
The rendered pages are at https://sg-groundtruth.vercel.app/findings.
