Repository navigation
Conversation
The product form now offers tags from a list Source curates rather than taking free text (source-cooperative/source.coop feat/product-tags), so describe picking them and how to ask for a missing one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
The tag field now opens a list to tick tags from rather than suggesting them as you type (source-cooperative/source.coop#647). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
alukach
added a commit
to source-cooperative/source.coop
that referenced
this pull request
Oct 26, 2026
…647) Product owners can now set a product's tags from the product form, in both create and edit mode, choosing from a curated corpus kept in a new DynamoDB table. ## What changes - **New table `sc-{stage}-tags`** (`deploy/lib/database-construct.ts`), partition key `tag_id`, which is the tag string itself (the same string stored in `product.metadata.tags`). Each row is just `{ tag_id }`. The Vercel role gets `grantReadData` only, because the app never writes the corpus. Curation happens outside the app. - **`TagsTable.listAll()`** (`src/lib/clients/database/tags.ts`) scans the table with pagination and returns the tags sorted. - **`TagPicker`** (`src/components/features/products/TagPicker.tsx`) shows the chosen tags as removable chips beside an "Edit tags" button. The button opens a Radix Themes `Popover` that lists every corpus tag as a checkbox, with a box to filter them, so people can browse the whole list without guessing what to type. Each chosen tag is submitted as its own `tags` form value from a hidden input next to its chip. It is wired into `ProductCreationForm` as a `custom` field under Description, and both pages pass the corpus in as `tagOptions`. - **Server enforcement** (`src/lib/operations/products.ts`): `createProduct` and `updateProduct` in the operations layer take an optional `tags` array, dedupe it, and reject any tag outside the corpus with a `tags` field error. A tag the product already carries is accepted even after it leaves the corpus. This follows the same legacy-drift rule as visibility: editing the title shouldn't fail on a tag nobody chose today. Because the API routes share these operations, `POST` and `PATCH` on products accept `tags` too, documented in the generated reference. The form actions in `src/lib/actions/products.ts` always pass `formData.getAll("tags")`; `Object.fromEntries(formData)` alone would keep only the last tag. - **Tag cleanup and seeding**: `scripts/clean-tags.ts <stage>` reads every product's free-text tags, cleans them, and builds the corpus from the result (see Tag cleanup below). It previews by default; `APPLY=1` rewrites each product's `metadata.tags` to its cleaned tags and puts the corpus into `sc-{stage}-tags`. `init-local` creates the table and seeds it from the fixture products' tags. ## Rollout 1. Deploy the CDK stack, which creates the table and the grant. 2. Run `AWS_REGION=us-west-2 npx tsx scripts/clean-tags.ts prod` and review the preview: the corpus, the dropped tags, and each product whose tags change. Production is in us-west-2; the script defaults to us-east-1 like the others in `scripts/`. Adjust `ALIASES` and `KEEP` in the script until the result is right. 3. Run it again with `APPLY=1`. This rewrites product tags, so take an on-demand backup of `sc-prod-products` first. Until then the corpus is empty, and the popover shows "No tags to choose from yet." ## Tag cleanup Production's products carry 416 distinct free-text tags. Many are the same tag in another case or spelling (`PMTiles` and `pmtiles`; `ftw` and `fieldsoftheworld`), several tags typed as one string (`#overturemaps #roads #riyadh …`), places below country level (14 Spanish regions, German state codes from the fiboa datasets), or noise (`test`, `foo`, `--`, an empty tag, and `geospatial` and `open data`, which are true of everything on Source). `scripts/clean-tags.ts` lowercases each tag, strips `#`, splits combined tags, maps synonyms, typos and places to one canonical tag through `ALIASES`, and drops noise. A canonical tag joins the corpus when two or more products carry it, or when it is in `KEEP` (countries, plus `elevation`, `machine learning`, `time series`, `forests` and `hydrology`). Each product keeps only its cleaned tags that are in the corpus. Run against the production counts (`TABLE=1 npx tsx scripts/clean-tags.ts --counts counts.txt`): **416 raw tags → 196 cleaned → 136 in corpus**. Counts are products per tag. They are built from per-tag counts, so a product that had two spellings of one tag counts twice and some totals run slightly high; the `<stage>` run counts per product exactly. <details><summary>The 136 tags in the corpus, by number of products</summary> | Tag | Products | | --- | --: | | `field boundaries` | 113 | | `geoparquet` | 101 | | `pmtiles` | 85 | | `agriculture` | 75 | | `vector` | 72 | | `fiboa` | 59 | | `fields of the world` | 58 | | `spain` | 30 | | `fieldscapes` | 27 | | `sentinel-2` | 25 | | `segmentation` | 20 | | `weather` | 18 | | `climate` | 17 | | `germany` | 17 | | `zarr` | 17 | | `building footprints` | 14 | | `energy` | 13 | | `hydrology` | 13 | | `sigpac` | 13 | | `crop type` | 12 | | `crops` | 12 | | `land cover` | 12 | | `raster` | 12 | | `united states` | 12 | | `cog` | 11 | | `satellite imagery` | 11 | | `overture maps` | 10 | | `saudi arabia` | 10 | | `biodiversity` | 9 | | `forecasting` | 9 | | `parquet` | 9 | | `sentinel-1` | 9 | | `admin boundaries` | 8 | | `places` | 8 | | `precipitation` | 8 | | `conservation` | 7 | | `elevation` | 7 | | `forests` | 7 | | `india` | 7 | | `labels` | 7 | | `machine learning` | 7 | | `carbon` | 6 | | `cloud native` | 6 | | `icechunk` | 6 | | `japan` | 6 | | `land use` | 6 | | `solar` | 6 | | `stac` | 6 | | `transportation` | 6 | | `vegetation` | 6 | | `embeddings` | 5 | | `global` | 5 | | `landsat` | 5 | | `ocean` | 5 | | `openstreetmap` | 5 | | `planetscope` | 5 | | `sar` | 5 | | `ai` | 4 | | `carbon dioxide removal` | 4 | | `flatgeobuf` | 4 | | `foundation models` | 4 | | `gapfilled` | 4 | | `lidar` | 4 | | `modis` | 4 | | `natural gas` | 4 | | `nhd` | 4 | | `remote sensing` | 4 | | `tanzania` | 4 | | `wildfire` | 4 | | `youthmappers` | 4 | | `aerosols` | 3 | | `atmosphere` | 3 | | `belgium` | 3 | | `canada` | 3 | | `census` | 3 | | `cer` | 3 | | `clay` | 3 | | `cloud` | 3 | | `ecology` | 3 | | `fisheries` | 3 | | `image classification` | 3 | | `imagery` | 3 | | `netherlands` | 3 | | `radar` | 3 | | `reanalysis` | 3 | | `regression` | 3 | | `rwanda` | 3 | | `africa` | 2 | | `austria` | 2 | | `bathymetry` | 2 | | `brazil` | 2 | | `computer vision` | 2 | | `duckdb` | 2 | | `eurocrops` | 2 | | `exports` | 2 | | `flood detection` | 2 | | `geopackage` | 2 | | `geostationary` | 2 | | `google` | 2 | | `h3` | 2 | | `hls` | 2 | | `imports` | 2 | | `invekos` | 2 | | `latvia` | 2 | | `marine debris` | 2 | | `microsoft` | 2 | | `monitoring` | 2 | | `naip` | 2 | | `neb` | 2 | | `noaa` | 2 | | `object detection` | 2 | | `observations` | 2 | | `slovenia` | 2 | | `switzerland` | 2 | | `tiles` | 2 | | `time series` | 2 | | `united kingdom` | 2 | | `usda` | 2 | | `wetlands` | 2 | | `wildlife` | 2 | | `antarctica` | 1 | | `cambodia` | 1 | | `croatia` | 1 | | `czechia` | 1 | | `denmark` | 1 | | `estonia` | 1 | | `europe` | 1 | | `finland` | 1 | | `france` | 1 | | `ireland` | 1 | | `luxembourg` | 1 | | `new zealand` | 1 | | `portugal` | 1 | | `slovakia` | 1 | | `sweden` | 1 | | `vietnam` | 1 | </details> <details><summary>Dropped because only one product carries them (60)</summary> | Tag | Products | | --- | --: | | `american bollworms` | 1 | | `attribution benchmark` | 1 | | `bioregionalisation` | 1 | | `brp` | 1 | | `catalog` | 1 | | `copernicus` | 1 | | `corridors` | 1 | | `csv` | 1 | | `digitaltwin` | 1 | | `disaster risk` | 1 | | `dwr` | 1 | | `ee-mrio` | 1 | | `egms` | 1 | | `eia` | 1 | | `environment` | 1 | | `environmental data` | 1 | | `epa` | 1 | | `equity` | 1 | | `esa` | 1 | | `exiobase` | 1 | | `ferc` | 1 | | `finance` | 1 | | `foodsecurity` | 1 | | `foodsystems` | 1 | | `fude` | 1 | | `gdal` | 1 | | `geocoding` | 1 | | `geos` | 1 | | `geozarr` | 1 | | `goes` | 1 | | `government` | 1 | | `hex` | 1 | | `historical` | 1 | | `human footprint` | 1 | | `iceberg` | 1 | | `imputation` | 1 | | `jpss` | 1 | | `justice` | 1 | | `landscape elements` | 1 | | `meiji era` | 1 | | `metop` | 1 | | `nasa` | 1 | | `neon` | 1 | | `ngff` | 1 | | `nsta` | 1 | | `pink bollworms` | 1 | | `plodiny` | 1 | | `png` | 1 | | `policy` | 1 | | `population` | 1 | | `procurement` | 1 | | `property data` | 1 | | `protected areas` | 1 | | `realtime` | 1 | | `sahara` | 1 | | `snow` | 1 | | `supplychain` | 1 | | `synthetic data` | 1 | | `terrestrial` | 1 | | `usgs` | 1 | Keep any of these by adding it to `KEEP`, or merge it into a corpus tag in `ALIASES`. </details> <details><summary>Every tag that changes (295 rows; unchanged tags appear only in the corpus list)</summary> | Count | Before | After | Notes | | --: | --- | --- | --- | | 30 | `ftw` | `fields of the world` | Merged | | 28 | `fieldsoftheworld` | `fields of the world` | Merged | | 10 | `geospatial` | — | Dropped: noise | | 5 | `boundaries` | `admin boundaries` | Merged | | 4 | `satellite` | `satellite imagery` | Merged | | 4 | `overture` | `overture maps` | Merged | | 4 | `landsat 8` | `landsat` | Merged | | 4 | `foundational model` | `foundation models` | Merged | | 3 | `nhd` | `nhd`, `hydrology` | Split into several tags | | 3 | `ml` | `machine learning` | Merged | | 3 | `imagery` | `satellite imagery` | Merged | | 3 | (empty) | — | Dropped: noise | | 2 | `usa` | `united states` | Merged | | 2 | `uk` | `united kingdom` | Merged | | 2 | `sentinel2` | `sentinel-2` | Merged | | 2 | `opendata` | — | Dropped: noise | | 2 | `karnataka` | `india` | Merged | | 2 | `forecast` | `forecasting` | Merged | | 2 | `fieldboundaries` | `field boundaries` | Merged | | 2 | `field boundary` | `field boundaries` | Merged | | 2 | `field` | — | Dropped: noise | | 2 | `earth` | — | Dropped: noise | | 2 | `dem` | `elevation` | Merged | | 2 | `crop` | `crops` | Merged | | 2 | `cloud optimised geotiff` | `cog` | Merged | | 2 | `climate change` | `climate` | Merged | | 2 | `cdr` | `carbon dioxide removal` | Merged | | 2 | `california` | `united states` | Merged | | 2 | `buildings` | `building footprints` | Merged | | 2 | `building footprint` | `building footprints` | Merged | | 2 | `admin` | `admin boundaries` | Merged | | 2 | `Sentinel2` | `sentinel-2` | Merged | | 2 | `PMTiles` | `pmtiles` | Normalized (case, #) | | 2 | `Japan` | `japan` | Normalized (case, #) | | 2 | `COG` | `cog` | Normalized (case, #) | | 2 | `AI` | `ai` | Normalized (case, #) | | 2 | `#geoparquet #aois #places #dataset #riyadh #geospatial #overturemaps #saudiarabia` | `geoparquet`, `places`, `saudi arabia`, `overture maps` | Split into several tags | | 1 | `youthmasppers` | `youthmappers` | Merged | | 1 | `years` | — | Dropped: noise | | 1 | `xai` | `machine learning` | Merged | | 1 | `wheat` | `crops` | Merged | | 1 | `wbd` | `hydrology` | Merged | | 1 | `waterways` | `hydrology` | Merged | | 1 | `watershed boundaries` | `hydrology` | Merged | | 1 | `water` | `hydrology` | Merged | | 1 | `wadhwaniai` | — | Dropped: noise | | 1 | `vegetation cover` | `vegetation` | Merged | | 1 | `valencia` | `spain` | Merged | | 1 | `usgs` | — | Dropped: on one product only | | 1 | `us` | `united states` | Merged | | 1 | `tropical storm` | `weather` | Merged | | 1 | `trees` | `forests` | Merged | | 1 | `topography` | `elevation` | Merged | | 1 | `timeseries` | `time series` | Merged | | 1 | `tiff` | `raster` | Merged | | 1 | `ticks` | `biodiversity` | Merged | | 1 | `th` | `germany` | Merged | | 1 | `testrepo12` | — | Dropped: noise | | 1 | `test unidata netcdf` | — | Dropped: noise | | 1 | `test` | — | Dropped: noise | | 1 | `terrestrial` | — | Dropped: on one product only | | 1 | `temperature` | `weather` | Merged | | 1 | `taxi zones` | `transportation` | Merged | | 1 | `tamsat` | `precipitation` | Merged | | 1 | `synthetic data` | — | Dropped: on one product only | | 1 | `swiss` | `switzerland` | Merged | | 1 | `supplychain` | — | Dropped: on one product only | | 1 | `street level imagery` | `imagery` | Merged | | 1 | `static` | — | Dropped: noise | | 1 | `state` | — | Dropped: noise | | 1 | `stac-geoparquet` | `stac`, `geoparquet` | Split into several tags | | 1 | `sst` | `ocean` | Merged | | 1 | `species distribution model` | `biodiversity` | Merged | | 1 | `southern ocean` | `ocean` | Merged | | 1 | `south fork eel river` | `united states` | Merged | | 1 | `source` | — | Dropped: noise | | 1 | `solar maps` | `solar` | Merged | | 1 | `snow` | — | Dropped: on one product only | | 1 | `slovenia fiboa field boundaries krms gerk geoparquet vector pmtiles` | `slovenia`, `fiboa`, `field boundaries`, `geoparquet`, `vector`, `pmtiles` | Split into several tags | | 1 | `sl` | `germany` | Merged | | 1 | `sixpac` | `sigpac` | Merged | | 1 | `sh` | `germany` | Merged | | 1 | `sfer` | — | Dropped: noise | | 1 | `self-supervised` | `machine learning` | Merged | | 1 | `sax` | — | Dropped: noise | | 1 | `saudiarabia` | `saudi arabia` | Merged | | 1 | `sahara` | — | Dropped: on one product only | | 1 | `riyadh` | `saudi arabia` | Merged | | 1 | `remotesensing` | `remote sensing` | Merged | | 1 | `reanalysis data` | `reanalysis` | Merged | | 1 | `realtime` | — | Dropped: on one product only | | 1 | `rainfall radar` | `precipitation`, `radar` | Split into several tags | | 1 | `pv` | `solar` | Merged | | 1 | `public-data` | — | Dropped: noise | | 1 | `public` | — | Dropped: noise | | 1 | `protected areas` | — | Dropped: on one product only | | 1 | `property data` | — | Dropped: on one product only | | 1 | `procurement` | — | Dropped: on one product only | | 1 | `population` | — | Dropped: on one product only | | 1 | `polygon` | — | Dropped: noise | | 1 | `political boundaries` | `admin boundaries` | Merged | | 1 | `policy` | — | Dropped: on one product only | | 1 | `pois` | `places` | Merged | | 1 | `poi` | `places` | Merged | | 1 | `png` | — | Dropped: on one product only | | 1 | `pmtiles gtfs` | `pmtiles`, `transportation` | Split into several tags | | 1 | `plodiny` | — | Dropped: on one product only | | 1 | `planet fusion` | `planetscope` | Merged | | 1 | `pink bollworms` | — | Dropped: on one product only | | 1 | `photovoltaic` | `solar` | Merged | | 1 | `perspective images` | `imagery` | Merged | | 1 | `pacificspatial` | — | Dropped: noise | | 1 | `overturemaps` | `overture maps` | Merged | | 1 | `osm` | `openstreetmap` | Merged | | 1 | `open data` | — | Dropped: noise | | 1 | `ome` | — | Dropped: noise | | 1 | `oil production` | `energy` | Merged | | 1 | `observation` | `observations` | Merged | | 1 | `nyc` | `united states` | Merged | | 1 | `nwp` | `forecasting`, `weather` | Split into several tags | | 1 | `nsta` | — | Dropped: on one product only | | 1 | `nrw` | `germany` | Merged | | 1 | `ngff` | — | Dropped: on one product only | | 1 | `netherlands fiboa field boundaries aan geoparquet vector pmtiles` | `netherlands`, `fiboa`, `field boundaries`, `geoparquet`, `vector`, `pmtiles` | Split into several tags | | 1 | `net zero` | `carbon` | Merged | | 1 | `neon` | — | Dropped: on one product only | | 1 | `nds` | `germany` | Merged | | 1 | `navarra` | `spain` | Merged | | 1 | `natural gas production` | `natural gas` | Merged | | 1 | `national hydrology dataset` | `nhd`, `hydrology` | Split into several tags | | 1 | `nasa` | — | Dropped: on one product only | | 1 | `mv` | `germany` | Merged | | 1 | `multitemporal` | `time series` | Merged | | 1 | `mrms` | `precipitation` | Merged | | 1 | `misc` | — | Dropped: noise | | 1 | `metop` | — | Dropped: on one product only | | 1 | `metadata` | — | Dropped: noise | | 1 | `maize` | `crops` | Merged | | 1 | `madrid` | `spain` | Merged | | 1 | `luxembourg fiboa field flik boundaries geoparquet vector pmtiles` | `luxembourg`, `fiboa`, `field boundaries`, `geoparquet`, `vector`, `pmtiles` | Split into several tags | | 1 | `lulc` | `land cover`, `land use` | Split into several tags | | 1 | `lowcoder` | — | Dropped: noise | | 1 | `live fuel moisture` | `wildfire` | Merged | | 1 | `lidar 3dtiles mago` | `lidar` | Merged | | 1 | `landscape elements` | — | Dropped: on one product only | | 1 | `landcover` | `land cover` | Merged | | 1 | `land-use and land-cover change` | `land cover`, `land use` | Split into several tags | | 1 | `land` | — | Dropped: noise | | 1 | `lacuna` | — | Dropped: noise | | 1 | `ksrsac` | `india` | Merged | | 1 | `karnatak` | `india` | Merged | | 1 | `justice` | — | Dropped: on one product only | | 1 | `jpss` | — | Dropped: on one product only | | 1 | `ireland fiboa field boundarie geoparquet vector pmtiles` | `ireland`, `fiboa`, `field boundaries`, `geoparquet`, `vector`, `pmtiles` | Split into several tags | | 1 | `insolation` | `solar` | Merged | | 1 | `imputation` | — | Dropped: on one product only | | 1 | `imerg` | `precipitation` | Merged | | 1 | `image` | — | Dropped: noise | | 1 | `iceberg` | — | Dropped: on one product only | | 1 | `hydrography` | `hydrology` | Merged | | 1 | `human footprint` | — | Dropped: on one product only | | 1 | `historical` | — | Dropped: on one product only | | 1 | `hex` | — | Dropped: on one product only | | 1 | `height` | `elevation` | Merged | | 1 | `hcat` | — | Dropped: noise | | 1 | `havasu` | `united states` | Merged | | 1 | `hamburg` | `germany` | Merged | | 1 | `groundwater quality` | `hydrology` | Merged | | 1 | `groundtruthed` | `labels` | Merged | | 1 | `ground-truthed` | `labels` | Merged | | 1 | `ground-truth` | `labels` | Merged | | 1 | `gpm` | `precipitation` | Merged | | 1 | `government` | — | Dropped: on one product only | | 1 | `goes` | — | Dropped: on one product only | | 1 | `geozarr` | — | Dropped: on one product only | | 1 | `geos` | — | Dropped: on one product only | | 1 | `geocoding` | — | Dropped: on one product only | | 1 | `geo spatial` | — | Dropped: noise | | 1 | `gdal` | — | Dropped: on one product only | | 1 | `galicia` | `spain` | Merged | | 1 | `fude` | — | Dropped: on one product only | | 1 | `forest carbon` | `forests`, `carbon` | Split into several tags | | 1 | `forest` | `forests` | Merged | | 1 | `forecasts` | `forecasting` | Merged | | 1 | `foodsystems` | — | Dropped: on one product only | | 1 | `foodsecurity` | — | Dropped: on one product only | | 1 | `foo` | — | Dropped: noise | | 1 | `flowlines` | `hydrology` | Merged | | 1 | `flanders` | `belgium` | Merged | | 1 | `fishbase` | `fisheries` | Merged | | 1 | `fish` | `fisheries` | Merged | | 1 | `fire` | `wildfire` | Merged | | 1 | `finance` | — | Dropped: on one product only | | 1 | `ferc` | — | Dropped: on one product only | | 1 | `farmland` | `agriculture` | Merged | | 1 | `extremadura` | `spain` | Merged | | 1 | `euhvd` | — | Dropped: noise | | 1 | `esa` | — | Dropped: on one product only | | 1 | `era5` | `reanalysis`, `climate` | Split into several tags | | 1 | `equity` | — | Dropped: on one product only | | 1 | `epa` | — | Dropped: on one product only | | 1 | `environmental data` | — | Dropped: on one product only | | 1 | `environment` | — | Dropped: on one product only | | 1 | `energy use by fuel` | `energy` | Merged | | 1 | `energy use` | `energy` | Merged | | 1 | `energy transition` | `energy` | Merged | | 1 | `energy production` | `energy` | Merged | | 1 | `energy efficiency` | `energy` | Merged | | 1 | `energy demand by sector` | `energy` | Merged | | 1 | `energy demand` | `energy` | Merged | | 1 | `emissions` | `carbon` | Merged | | 1 | `embedding` | `embeddings` | Merged | | 1 | `electricity generation` | `energy` | Merged | | 1 | `electricity` | `energy` | Merged | | 1 | `eia` | — | Dropped: on one product only | | 1 | `eatch monitoring` | `monitoring` | Merged | | 1 | `dwr` | — | Dropped: on one product only | | 1 | `dust` | `aerosols` | Merged | | 1 | `drone` | `imagery` | Merged | | 1 | `disaster risk` | — | Dropped: on one product only | | 1 | `digitaltwin` | — | Dropped: on one product only | | 1 | `deforestation` | `forests` | Merged | | 1 | `deep learning` | `machine learning` | Merged | | 1 | `dataset` | — | Dropped: noise | | 1 | `czech` | `czechia` | Merged | | 1 | `csv` | — | Dropped: on one product only | | 1 | `croptype` | `crop type` | Merged | | 1 | `county` | — | Dropped: noise | | 1 | `countries` | — | Dropped: noise | | 1 | `cotton` | `crops` | Merged | | 1 | `corridors` | — | Dropped: on one product only | | 1 | `contour` | `elevation` | Merged | | 1 | `coal` | `energy` | Merged | | 1 | `cloud-native` | `cloud native` | Merged | | 1 | `cdl` | `crop type`, `united states` | Split into several tags | | 1 | `catalonia` | `spain` | Merged | | 1 | `catalog` | — | Dropped: on one product only | | 1 | `castillalamancha` | `spain` | Merged | | 1 | `castile and leon` | `spain` | Merged | | 1 | `carbon emissions` | `carbon` | Merged | | 1 | `cantabria` | `spain` | Merged | | 1 | `canary islands` | `spain` | Merged | | 1 | `businesses` | `places` | Merged | | 1 | `building` | `building footprints` | Merged | | 1 | `brp` | — | Dropped: on one product only | | 1 | `bremen` | `germany` | Merged | | 1 | `boundary vector agriculture` | `field boundaries`, `vector`, `agriculture` | Split into several tags | | 1 | `blore` | `india` | Merged | | 1 | `bioregionalisation` | — | Dropped: on one product only | | 1 | `biomass` | `forests` | Merged | | 1 | `belgium wallonia fiboa field boundaries geoparquet vector pmtiles` | `belgium`, `fiboa`, `field boundaries`, `geoparquet`, `vector`, `pmtiles` | Split into several tags | | 1 | `beetles` | `biodiversity` | Merged | | 1 | `bb` | `germany` | Merged | | 1 | `basque` | `spain` | Merged | | 1 | `balearic islands` | `spain` | Merged | | 1 | `attribution benchmark` | — | Dropped: on one product only | | 1 | `arc` | — | Dropped: noise | | 1 | `aragon` | `spain` | Merged | | 1 | `aquatics` | `ocean` | Merged | | 1 | `aois` | — | Dropped: noise | | 1 | `andalusia` | `spain` | Merged | | 1 | `andalucía` | `spain` | Merged | | 1 | `analysis` | — | Dropped: noise | | 1 | `amx` | — | Dropped: noise | | 1 | `american bollworms` | — | Dropped: on one product only | | 1 | `aerosol` | `aerosols` | Merged | | 1 | `Trees` | `forests` | Merged | | 1 | `Tottori` | `japan` | Merged | | 1 | `Topographic Map` | `elevation` | Merged | | 1 | `Sentinel2 mosaics cloud-free global cog raster` | `sentinel-2`, `global`, `cog`, `raster` | Split into several tags | | 1 | `PMTiles FlatGeobuf` | `pmtiles`, `flatgeobuf` | Split into several tags | | 1 | `Meiji era` | — | Dropped: on one product only | | 1 | `Forest` | `forests` | Merged | | 1 | `EXIOBASE` | — | Dropped: on one product only | | 1 | `EU` | `europe` | Merged | | 1 | `EGMS` | — | Dropped: on one product only | | 1 | `EE-MRIO` | — | Dropped: on one product only | | 1 | `DEM` | `elevation` | Merged | | 1 | `Copernicus` | — | Dropped: on one product only | | 1 | `Cloud-Native` | `cloud native` | Merged | | 1 | `3d tiles` | `lidar` | Merged | | 1 | `--` | — | Dropped: noise | | 1 | `#waterways` | `hydrology` | Merged | | 1 | `#vector` | `vector` | Normalized (case, #) | | 1 | `#tif` | `raster` | Merged | | 1 | `#stac` | `stac` | Normalized (case, #) | | 1 | `#sentinel #satellite #imagery #cog #aws #remotesensing #saudiarabia #tabaqat` | `sentinel-2`, `satellite imagery`, `cog`, `remote sensing`, `saudi arabia` | Split into several tags | | 1 | `#riyadh #satellite #rgb #cog #2024` | `saudi arabia`, `satellite imagery`, `cog` | Split into several tags | | 1 | `#raster` | `raster` | Normalized (case, #) | | 1 | `#pmtiles #cng #saudiarabia #sentinel #tabaqat #aws` | `pmtiles`, `cloud native`, `saudi arabia`, `sentinel-2` | Split into several tags | | 1 | `#parquet` | `parquet` | Normalized (case, #) | | 1 | `#overturemaps #roads #riyadh #osm #pmtiles #cng #tabaqat` | `overture maps`, `transportation`, `saudi arabia`, `openstreetmap`, `pmtiles`, `cloud native` | Split into several tags | | 1 | `#overturemaps #roads #pmtiles #cng #osm #tabaqat #saudiarabia` | `overture maps`, `transportation`, `pmtiles`, `cloud native`, `openstreetmap`, `saudi arabia` | Split into several tags | | 1 | `#geospatial #ksa #riyadh #geoparquet #places #pois` | `saudi arabia`, `geoparquet`, `places` | Split into several tags | | 1 | `#cbam #climate reanalysis` | `reanalysis`, `climate` | Split into several tags | </details> ## Decisions worth a second look - **Which tags survive is a judgement call.** The `ALIASES` and `KEEP` lists in `scripts/clean-tags.ts` encode it, and the tables above show the result. Calls most worth checking: `boundaries` and `admin` become `admin boundaries`; `satellite` and `imagery` merge into `satellite imagery`; agencies on one product (`nasa`, `usgs`, `esa`, `epa`) are dropped while those on two or more (`noaa`, `usda`, `cer`, `neb`) stay. - **No admin UI for the corpus.** Tags are added in the AWS console or CLI for now. An admin page is the natural follow-up if curation becomes frequent. - **Leaving `tags` out of an update keeps them; an empty list clears them.** That suits API callers, who shouldn't wipe a product's tags by editing its title. The form always sends the list, because removing every chip submits no `tags` field at all, and that has to clear them. - **No cap on tag count.** The docs used to promise "up to 20". Nothing enforced that before and nothing does now, so I dropped the claim from the docs instead of inventing a limit. - **Popover picker instead of a datalist or a combobox library.** The first version used a native `<datalist>`, but its dropdown ignored the theme and hid the list until you typed. Of the alternatives (every tag as a toggle chip, or a popover like GitHub's label picker), the popover keeps the field compact and uses only components Radix Themes already ships. The list renders every tag without virtualisation, which is fine to a few hundred tags; past that it should be virtualised (marked with a `ponytail:` comment). ## Stories On this branch's Storybook deploy: - TagPicker: [Empty](https://source-coop-ui-git-feat-product-tags-radiantearth.vercel.app/?path=/story/features-settings-details-tagpicker--empty), [WithTags](https://source-coop-ui-git-feat-product-tags-radiantearth.vercel.app/?path=/story/features-settings-details-tagpicker--with-tags), [RetiredTag](https://source-coop-ui-git-feat-product-tags-radiantearth.vercel.app/?path=/story/features-settings-details-tagpicker--retired-tag), [Filtering](https://source-coop-ui-git-feat-product-tags-radiantearth.vercel.app/?path=/story/features-settings-details-tagpicker--filtering) (opens the popover and filters it) - ProductCreationForm: [Create](https://source-coop-ui-git-feat-product-tags-radiantearth.vercel.app/?path=/story/features-settings-details-productcreationform--create), [Edit](https://source-coop-ui-git-feat-product-tags-radiantearth.vercel.app/?path=/story/features-settings-details-productcreationform--edit) The Filtering story, with the popover open and filtered to tags containing "c":  ## Docs - `docs/using-source/create-a-data-product.md` described tags as "Comma-separated, up to 20 tags", which is now stale. Updated in [docs.source.coop#39](source-cooperative/docs.source.coop#39). - `docs/about-source/core-concepts.md` ("Tags and Discoverability") still holds: it says what tags are for, not how they are entered. - `public/llms.txt` doesn't mention tags, and this adds no public endpoint. Checked the data-proxy ADRs: tags have no bearing on authorization, credentials, or federation. ## Testing - `npm run test` for `src/lib/actions/products.test.ts` and `src/components/features/products/TagPicker.test.tsx` passes locally (30 tests), and so does `npm run type-check`. `npm run lint` reports nothing in the changed files. - The action tests cover create saving corpus tags, rejecting unknown ones, and not reading the tag table when no tags are sent, and update saving corpus tags, rejecting unknown ones, and keeping a retired tag the product already had. - `TagPicker.test.tsx` covers removing a chip, and the popover filtering case-insensitively and toggling a tag on and off. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Tyler Erickson <92862+tylere@users.noreply.github.com>
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The product form now offers tags from a list Source curates instead of taking comma-separated free text (source-cooperative/source.coop#647). This updates the Tags bullet in Create a data product to describe picking them, and drops the "up to 20" limit, which the app never enforced. The "contact hello@source.coop" line for a missing tag is a placeholder until there's a real request path, so correct it if there's a better one. Merge alongside #647.
🤖 Generated with Claude Code