@withpica/mcp-server 2.97.5 → 2.99.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/CHANGELOG.md +223 -0
  2. package/README.intro.md +1 -1
  3. package/README.md +8 -4
  4. package/dist/lib/changelog.generated.d.ts +2 -2
  5. package/dist/lib/changelog.generated.d.ts.map +1 -1
  6. package/dist/lib/changelog.generated.js +2 -2
  7. package/dist/lib/changelog.generated.js.map +1 -1
  8. package/dist/prompts/creator-question-atlas.d.ts.map +1 -1
  9. package/dist/prompts/creator-question-atlas.js +37 -2
  10. package/dist/prompts/creator-question-atlas.js.map +1 -1
  11. package/dist/prompts/index.d.ts.map +1 -1
  12. package/dist/prompts/index.js +18 -21
  13. package/dist/prompts/index.js.map +1 -1
  14. package/dist/resources/agent-guide.d.ts.map +1 -1
  15. package/dist/resources/agent-guide.js +3 -1
  16. package/dist/resources/agent-guide.js.map +1 -1
  17. package/dist/resources/index.d.ts.map +1 -1
  18. package/dist/resources/index.js +0 -3
  19. package/dist/resources/index.js.map +1 -1
  20. package/dist/resources/required-schemas.generated.d.ts +7 -5
  21. package/dist/resources/required-schemas.generated.d.ts.map +1 -1
  22. package/dist/resources/required-schemas.generated.js +15 -2
  23. package/dist/resources/required-schemas.generated.js.map +1 -1
  24. package/dist/resources/required-schemas.source.d.ts.map +1 -1
  25. package/dist/resources/required-schemas.source.js +8 -1
  26. package/dist/resources/required-schemas.source.js.map +1 -1
  27. package/dist/server-instructions.d.ts +2 -2
  28. package/dist/server-instructions.d.ts.map +1 -1
  29. package/dist/server-instructions.js +1 -1
  30. package/dist/skills/skills.generated.js +11 -11
  31. package/dist/skills/skills.generated.js.map +1 -1
  32. package/dist/tools/agreement-types.d.ts.map +1 -1
  33. package/dist/tools/agreement-types.js +3 -1
  34. package/dist/tools/agreement-types.js.map +1 -1
  35. package/dist/tools/agreements.d.ts.map +1 -1
  36. package/dist/tools/agreements.js +15 -6
  37. package/dist/tools/agreements.js.map +1 -1
  38. package/dist/tools/approvals.d.ts.map +1 -1
  39. package/dist/tools/approvals.js +14 -1
  40. package/dist/tools/approvals.js.map +1 -1
  41. package/dist/tools/arg-aliases.d.ts.map +1 -1
  42. package/dist/tools/arg-aliases.js +12 -0
  43. package/dist/tools/arg-aliases.js.map +1 -1
  44. package/dist/tools/audio-files.d.ts +39 -0
  45. package/dist/tools/audio-files.d.ts.map +1 -1
  46. package/dist/tools/audio-files.js +253 -12
  47. package/dist/tools/audio-files.js.map +1 -1
  48. package/dist/tools/catalog-enums.d.ts +20 -0
  49. package/dist/tools/catalog-enums.d.ts.map +1 -0
  50. package/dist/tools/catalog-enums.js +28 -0
  51. package/dist/tools/catalog-enums.js.map +1 -0
  52. package/dist/tools/collaborators.d.ts.map +1 -1
  53. package/dist/tools/collaborators.js +27 -3
  54. package/dist/tools/collaborators.js.map +1 -1
  55. package/dist/tools/credits.d.ts +4 -1
  56. package/dist/tools/credits.d.ts.map +1 -1
  57. package/dist/tools/credits.js +70 -97
  58. package/dist/tools/credits.js.map +1 -1
  59. package/dist/tools/custody-hints.js +2 -2
  60. package/dist/tools/custody-hints.js.map +1 -1
  61. package/dist/tools/custody.d.ts.map +1 -1
  62. package/dist/tools/custody.js +4 -1
  63. package/dist/tools/custody.js.map +1 -1
  64. package/dist/tools/deprecations.d.ts +67 -8
  65. package/dist/tools/deprecations.d.ts.map +1 -1
  66. package/dist/tools/deprecations.js +98 -82
  67. package/dist/tools/deprecations.js.map +1 -1
  68. package/dist/tools/directory.js +2 -2
  69. package/dist/tools/directory.js.map +1 -1
  70. package/dist/tools/discovery.d.ts +28 -2
  71. package/dist/tools/discovery.d.ts.map +1 -1
  72. package/dist/tools/discovery.js +72 -28
  73. package/dist/tools/discovery.js.map +1 -1
  74. package/dist/tools/documents.d.ts +2 -2
  75. package/dist/tools/documents.d.ts.map +1 -1
  76. package/dist/tools/documents.js +2 -88
  77. package/dist/tools/documents.js.map +1 -1
  78. package/dist/tools/duplicates.d.ts +2 -0
  79. package/dist/tools/duplicates.d.ts.map +1 -1
  80. package/dist/tools/duplicates.js +173 -18
  81. package/dist/tools/duplicates.js.map +1 -1
  82. package/dist/tools/enrichment.d.ts.map +1 -1
  83. package/dist/tools/enrichment.js +36 -2
  84. package/dist/tools/enrichment.js.map +1 -1
  85. package/dist/tools/import.d.ts.map +1 -1
  86. package/dist/tools/import.js +19 -0
  87. package/dist/tools/import.js.map +1 -1
  88. package/dist/tools/index.d.ts.map +1 -1
  89. package/dist/tools/index.js +44 -4
  90. package/dist/tools/index.js.map +1 -1
  91. package/dist/tools/integrity.d.ts.map +1 -1
  92. package/dist/tools/integrity.js +65 -8
  93. package/dist/tools/integrity.js.map +1 -1
  94. package/dist/tools/licensing.d.ts.map +1 -1
  95. package/dist/tools/licensing.js +14 -9
  96. package/dist/tools/licensing.js.map +1 -1
  97. package/dist/tools/metadata.d.ts.map +1 -1
  98. package/dist/tools/metadata.js +74 -114
  99. package/dist/tools/metadata.js.map +1 -1
  100. package/dist/tools/notes.d.ts +3 -4
  101. package/dist/tools/notes.d.ts.map +1 -1
  102. package/dist/tools/notes.js +31 -44
  103. package/dist/tools/notes.js.map +1 -1
  104. package/dist/tools/people.d.ts +5 -1
  105. package/dist/tools/people.d.ts.map +1 -1
  106. package/dist/tools/people.js +8 -1
  107. package/dist/tools/people.js.map +1 -1
  108. package/dist/tools/projects.d.ts +3 -4
  109. package/dist/tools/projects.d.ts.map +1 -1
  110. package/dist/tools/projects.js +3 -27
  111. package/dist/tools/projects.js.map +1 -1
  112. package/dist/tools/public-filter.d.ts.map +1 -1
  113. package/dist/tools/public-filter.js +3 -6
  114. package/dist/tools/public-filter.js.map +1 -1
  115. package/dist/tools/purchases.d.ts +2 -3
  116. package/dist/tools/purchases.d.ts.map +1 -1
  117. package/dist/tools/purchases.js.map +1 -1
  118. package/dist/tools/recording-attribution-hints.js +1 -1
  119. package/dist/tools/recording-attribution-hints.js.map +1 -1
  120. package/dist/tools/recording-custody.d.ts +3 -1
  121. package/dist/tools/recording-custody.d.ts.map +1 -1
  122. package/dist/tools/recording-custody.js +8 -4
  123. package/dist/tools/recording-custody.js.map +1 -1
  124. package/dist/tools/recovery-hints.d.ts +0 -6
  125. package/dist/tools/recovery-hints.d.ts.map +1 -1
  126. package/dist/tools/recovery-hints.js +97 -38
  127. package/dist/tools/recovery-hints.js.map +1 -1
  128. package/dist/tools/register.d.ts.map +1 -1
  129. package/dist/tools/register.js +2 -1
  130. package/dist/tools/register.js.map +1 -1
  131. package/dist/tools/releases.d.ts +2 -4
  132. package/dist/tools/releases.d.ts.map +1 -1
  133. package/dist/tools/releases.js +12 -213
  134. package/dist/tools/releases.js.map +1 -1
  135. package/dist/tools/rights.d.ts +54 -0
  136. package/dist/tools/rights.d.ts.map +1 -0
  137. package/dist/tools/rights.js +495 -0
  138. package/dist/tools/rights.js.map +1 -0
  139. package/dist/tools/royalties.d.ts +29 -6
  140. package/dist/tools/royalties.d.ts.map +1 -1
  141. package/dist/tools/royalties.js +101 -119
  142. package/dist/tools/royalties.js.map +1 -1
  143. package/dist/tools/section-validation.d.ts +14 -0
  144. package/dist/tools/section-validation.d.ts.map +1 -0
  145. package/dist/tools/section-validation.js +68 -0
  146. package/dist/tools/section-validation.js.map +1 -0
  147. package/dist/tools/selections.d.ts +5 -2
  148. package/dist/tools/selections.d.ts.map +1 -1
  149. package/dist/tools/selections.js +10 -4
  150. package/dist/tools/selections.js.map +1 -1
  151. package/dist/tools/share-links.d.ts.map +1 -1
  152. package/dist/tools/share-links.js +32 -6
  153. package/dist/tools/share-links.js.map +1 -1
  154. package/dist/tools/share-send.d.ts.map +1 -1
  155. package/dist/tools/share-send.js +32 -7
  156. package/dist/tools/share-send.js.map +1 -1
  157. package/dist/tools/signed-split-sheet-preview.d.ts +43 -0
  158. package/dist/tools/signed-split-sheet-preview.d.ts.map +1 -0
  159. package/dist/tools/signed-split-sheet-preview.js +67 -0
  160. package/dist/tools/signed-split-sheet-preview.js.map +1 -0
  161. package/dist/tools/split-sheets.d.ts.map +1 -1
  162. package/dist/tools/split-sheets.js +3 -2
  163. package/dist/tools/split-sheets.js.map +1 -1
  164. package/dist/tools/statements.js +1 -1
  165. package/dist/tools/statements.js.map +1 -1
  166. package/dist/tools/version-guard.d.ts +16 -4
  167. package/dist/tools/version-guard.d.ts.map +1 -1
  168. package/dist/tools/version-guard.js +22 -8
  169. package/dist/tools/version-guard.js.map +1 -1
  170. package/dist/tools/works.d.ts +9 -1
  171. package/dist/tools/works.d.ts.map +1 -1
  172. package/dist/tools/works.js +217 -74
  173. package/dist/tools/works.js.map +1 -1
  174. package/dist/tools/write-receipt.d.ts +68 -0
  175. package/dist/tools/write-receipt.d.ts.map +1 -0
  176. package/dist/tools/write-receipt.js +64 -0
  177. package/dist/tools/write-receipt.js.map +1 -0
  178. package/package.json +4 -4
  179. package/scripts/build-required-schemas.ts +7 -1
  180. package/server.json +2 -2
@@ -21,9 +21,12 @@
21
21
  import { ApiError, EntityNotFoundError, } from "@withpica/mcp-sdk";
22
22
  import { formatSuccess, formatDeletion, formatStructuredList, withBillingGate, buildDecision, mapGapsToHints, pushSectionError, } from "@withpica/mcp-utils";
23
23
  import { buildCustodyHints } from "./custody-hints.js";
24
+ import { typedError } from "./error-envelope.js";
24
25
  import { computeWorkGapHints } from "./gap-hints.js";
25
26
  import { buildIntegrityPressure } from "./integrity-hints.js";
26
27
  import { detectVersionOfExistingWork, buildVersionWarning, parseVersionTitle, } from "./version-guard.js";
28
+ import { WORK_TYPE_VALUES } from "./catalog-enums.js";
29
+ import { VERBOSE_PROPERTY, RECEIPT_NOTE, changedFields, newHintsOnly, slimIntegrity, takeVerbose, } from "./write-receipt.js";
27
30
  /**
28
31
  * Canonical write-schema for `works`. Every property below must
29
32
  * correspond to a real column on `public.works` — verified against
@@ -41,7 +44,7 @@ export const WORK_WRITE_PROPERTIES = {
41
44
  work_type: {
42
45
  type: "string",
43
46
  description: "Type of work",
44
- enum: ["song", "instrumental", "library", "demo", "sample"],
47
+ enum: [...WORK_TYPE_VALUES],
45
48
  },
46
49
  work_status: {
47
50
  type: "string",
@@ -120,6 +123,22 @@ export const WORK_WRITE_PROPERTIES = {
120
123
  type: "string",
121
124
  description: "Primary artist person ID (FK to people)",
122
125
  },
126
+ disambiguation: {
127
+ type: ["string", "null"],
128
+ maxLength: 200,
129
+ description: "A short note that tells apart two DIFFERENT songs with the same title and primary artist — e.g. 'theme', '2019 song', 'live-only composition' (MusicBrainz's term). " +
130
+ "Set it ONLY when two genuinely different songs share title + artist; the cue is a WORK_ALREADY_EXISTS refusal for a song you know is not the existing one. " +
131
+ "Leave it unset otherwise — it is part of the duplicate check, so an invented note would let a real duplicate in. " +
132
+ "If the two are the same song, use or merge the existing work instead. Not a version label: recording versions (live, remix, acoustic) belong on pica_recordings_create. " +
133
+ "null or '' clears it.",
134
+ },
135
+ original_work_id: {
136
+ type: ["string", "null"],
137
+ description: "Set ONLY on a MODIFIED VERSION: the id of the work this one modifies (CWR version type MOD). " +
138
+ "A remix is by default NOT a separate work — it is a new recording of the same song (pica_recordings_create with work_id of the original and version_type 'remix', remixer credited on the recording). " +
139
+ "Only when the remix adds writers the original does not have (new lyrics, melody or section) is it its own work with its own ISWC and writer shares; then set original_work_id to the original's id. " +
140
+ "Must be a work in the same catalogue, not this work. null clears the link.",
141
+ },
123
142
  display_artist: {
124
143
  type: "string",
125
144
  description: "Display-friendly artist string (e.g. 'Jane Doe ft. John Smith')",
@@ -142,7 +161,7 @@ export const WORK_WRITE_PROPERTIES = {
142
161
  },
143
162
  notes: {
144
163
  type: "string",
145
- description: "Internal notes",
164
+ description: "Internal notes. Setting this REPLACES the whole field — to add a line, use append_notes on pica_works_update instead.",
146
165
  },
147
166
  metadata: {
148
167
  type: "object",
@@ -200,6 +219,14 @@ function summariseDossier(dossier) {
200
219
  `Consent: ${consentState}${consentLevel}. Completeness: ${completeness}. ` +
201
220
  `(Full payload under structuredContent.data.dossier; PDF / multi-work ZIP: pica_export_song_dossiers.)`);
202
221
  }
222
+ /**
223
+ * Page caps for the two health_filter branches of pica_works_query — the
224
+ * routes' own maxima (app/api/admin/quality/works-health/route.ts and
225
+ * .../completeness/low-score/route.ts). Hand-copied (this package cannot
226
+ * import the app); no test ties them — change both together.
227
+ */
228
+ const WORKS_HEALTH_MAX_PAGE = 50;
229
+ const LOW_COMPLETENESS_MAX_PAGE = 200;
203
230
  export class WorksTools {
204
231
  pica;
205
232
  constructor(pica) {
@@ -220,7 +247,7 @@ export class WorksTools {
220
247
  "pending registration?', 'unregistered works', 'works registered " +
221
248
  "with ASCAP', 'least-complete works', 'works ready for outreach', " +
222
249
  "'works with no statement data', 'most-streamed works'. Search " +
223
- "by title, ISWC, or artist, or filter by verified status, " +
250
+ "by title, ISWC, or artist, or filter by " +
224
251
  "registration status, the collection society a work is registered " +
225
252
  "with (society_code, any territory), health, whether a collecting-" +
226
253
  "society statement is linked (has_statement_data), completeness range " +
@@ -247,16 +274,12 @@ export class WorksTools {
247
274
  properties: {
248
275
  query: {
249
276
  type: "string",
250
- description: "Search by title, ISWC, or artist name",
251
- },
252
- verified: {
253
- type: "boolean",
254
- description: "Filter by verification status",
277
+ description: "Search by title, ISWC, or artist name. An artist name also finds works linked to that artist through a recording's artist or a credited person, even when the work's primary_artist is empty or spelled differently (appended after the direct matches, up to 300).",
255
278
  },
256
279
  health_filter: {
257
280
  type: "string",
258
281
  enum: ["needs_attention", "low_completeness"],
259
- description: "Filter by health status — needs_attention (cross-org reconciliation problems: unreconciled composition/master chains, split discrepancies) or low_completeness (below completeness score threshold: missing ISWC, credits, etc.)",
282
+ description: "Filter by health status — needs_attention (cross-org reconciliation problems: unreconciled composition/master chains, split discrepancies) or low_completeness (below completeness score threshold: missing ISWC, credits, etc.). Both honour limit/offset. low_completeness reports `total` = works below the threshold by their stored score (max 200 per page). needs_attention checks at most 50 works per call and `offset` counts works CHECKED, not rows returned — a page can hold fewer rows than limit while more remain, so continue from next_offset until it is null; its `total` is set only when one call from offset 0 checks every candidate, otherwise it is null and the answer is the rows counted across pages.",
260
283
  },
261
284
  has_statement_data: {
262
285
  type: "boolean",
@@ -304,7 +327,7 @@ export class WorksTools {
304
327
  },
305
328
  work_type: {
306
329
  type: "string",
307
- enum: ["song", "instrumental", "library", "demo", "sample"],
330
+ enum: [...WORK_TYPE_VALUES],
308
331
  description: "Filter by work type. 'instrumental' answers 'my instrumentals'.",
309
332
  },
310
333
  work_status: {
@@ -337,11 +360,11 @@ export class WorksTools {
337
360
  },
338
361
  primary_artist: {
339
362
  type: "string",
340
- description: "Exact primary-artist name match.",
363
+ description: "Exact primary-artist name match. Misses works whose primary_artist is empty — use `query` to include works linked by recording artist or credits.",
341
364
  },
342
365
  artist: {
343
366
  type: "string",
344
- description: "Exact primary-artist name match (alias).",
367
+ description: "Exact primary-artist name match (alias). Misses works whose primary_artist is empty — use `query` to include works linked by recording artist or credits.",
345
368
  },
346
369
  label: {
347
370
  type: "string",
@@ -468,7 +491,7 @@ export class WorksTools {
468
491
  "dossier",
469
492
  ],
470
493
  },
471
- description: 'Which data sections to include. Omit for all sections. Use ["basic"] for lightweight lookup. "licensing" surfaces the AI-disclosure / training-rights / provenance-attestation compliance fields (work_licensing satellite) in one scoped view. "dossier" assembles the full song dossier (recordings, releases, credits, people, provenance, AI disclosure, consent) — heavy, so it is opt-in only (never part of the default all-sections response); for the PDF, or several works in one ZIP, use pica_export_song_dossiers.',
494
+ description: 'Which data sections to include. Omit for all sections. Use ["basic"] for lightweight lookup. "health" is a short summary of THIS work\'s completeness (score, missing critical fields, suggestions) — opt-in, since "completeness" carries the full breakdown; for works across the catalogue needing attention use pica_works_query health_filter. "licensing" surfaces the AI-disclosure / training-rights / provenance-attestation compliance fields (work_licensing satellite) in one scoped view. "dossier" assembles the full song dossier (recordings, releases, credits, people, provenance, AI disclosure, consent) — heavy, so it is opt-in only (never part of the default all-sections response); for the PDF, or several works in one ZIP, use pica_export_song_dossiers.',
472
495
  },
473
496
  },
474
497
  required: [],
@@ -515,7 +538,12 @@ export class WorksTools {
515
538
  "mastering human-made, AI-assisted, or AI-generated? (Those four " +
516
539
  "are set independently on ai_disclosure.) Always run " +
517
540
  "pica_works_query first to check for a duplicate — two same-title " +
518
- "works cause split and registration trouble. Album, artwork, " +
541
+ "works cause split and registration trouble. If a refusal says the " +
542
+ "work already exists but it really is a different song with the same " +
543
+ "title and artist, retry with disambiguation (e.g. 'theme'). A remix is " +
544
+ "usually a new recording of the original song (pica_recordings_create, " +
545
+ "version_type 'remix'), not a new work; only a remix that added writers " +
546
+ "is its own work, linked with original_work_id. Album, artwork, " +
519
547
  "track number, UPC, and Spotify URL live on the release " +
520
548
  "(pica_releases_create/update), not here; label too, via the " +
521
549
  "release (resolve the label name with pica_labels_query first). " +
@@ -570,6 +598,8 @@ export class WorksTools {
570
598
  "MCP; set them through a release or recording path. " +
571
599
  "Three jsonb compliance fields (ai_disclosure, provenance_attestation, training_rights) route " +
572
600
  "to that same work_licensing satellite table and ARE set-able here. " +
601
+ "original_work_id links a modified version (a remix that added writers) to the work it modifies. " +
602
+ "Answers with a compact receipt — the id, each field you sent with the value now stored, and only the gaps this write opened; pass verbose: true for the full work. " +
573
603
  "→ then: pica_works_inspect (verify changes persisted)",
574
604
  workflows: ["work-required"],
575
605
  vernacular_kind: "specialist",
@@ -582,6 +612,14 @@ export class WorksTools {
582
612
  description: "Work ID (required)",
583
613
  },
584
614
  ...WORK_WRITE_PROPERTIES,
615
+ ...VERBOSE_PROPERTY,
616
+ append_notes: {
617
+ type: "string",
618
+ maxLength: 10000,
619
+ description: "Text to add to the end of the work's notes, on a new line, without reading or rewriting what is there. " +
620
+ "Safe when someone else edits the work at the same time (a conflict is retried, never overwritten). " +
621
+ "Cannot be sent together with notes.",
622
+ },
585
623
  },
586
624
  required: ["id"],
587
625
  },
@@ -609,7 +647,7 @@ export class WorksTools {
609
647
  description: "Delete a musical work. It is automatically unlinked from any " +
610
648
  "releases it appears on (the release keeps its track). If the work " +
611
649
  "still has dependent records (recordings, royalty payments, version " +
612
- "links, claims) it is archived instead — hidden from the catalog but " +
650
+ "links, claims, signed split sheets) it is archived instead — hidden from the catalog but " +
613
651
  "preserved — and the result tells you exactly what to remove to " +
614
652
  "delete it permanently. A work with registry identifiers " +
615
653
  "(ISRC/ISWC/MLC) is always archived, never destroyed. The result " +
@@ -637,8 +675,11 @@ export class WorksTools {
637
675
  scope: "write:catalog",
638
676
  audiences: ["stdio", "http", "public"],
639
677
  tier: "write",
640
- description: "Mark a work's metadata as human-verified after user review — a catalog quality flag, " +
641
- "not a registration check (that's pica_works_verify_registrations).",
678
+ description: "Writes nothing: PICA keeps no single 'verified' flag on a work. Returns " +
679
+ "NO_WORK_VERIFIED_FLAG naming what the person can confirm themselves, signed in, " +
680
+ "on /inspect — the human-made stamp, their own writer credit, their own master " +
681
+ "share. Not a registration check (that's pica_works_verify_registrations). To " +
682
+ "review a work, use pica_works_inspect.",
642
683
  workflows: "infrastructure",
643
684
  vernacular_kind: "specialist",
644
685
  vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4 substrate writes.",
@@ -669,7 +710,11 @@ export class WorksTools {
669
710
  "how, and an optional signature hash (the raw signature itself is " +
670
711
  "never stored). Sync libraries and provenance certificates use it " +
671
712
  "to prove human authorship. To change or undo one, use the " +
672
- "provenance revocation tools. After stamping, read it back with " +
713
+ "provenance revocation tools. Only a signed-in person can make " +
714
+ "this statement: an API key, connected app or agent is refused " +
715
+ "with ATTESTATION_REQUIRES_HUMAN_SESSION — do not retry; tell the " +
716
+ "user to use 'stamp as human-made' at /inspect/works/<id>#human-made. " +
717
+ "After stamping, read it back with " +
673
718
  "pica_works_inspect (sections: ['provenance']). → then: " +
674
719
  "pica_works_inspect (verify it persisted), " +
675
720
  "pica_provenance_certificate (generate a PDF)",
@@ -743,7 +788,7 @@ export class WorksTools {
743
788
  "mode 'confirm_unregistered' records the user's own confirmation instead (no lookup; " +
744
789
  "society_code optional — omitted means 'not registered with any society'). " +
745
790
  "Idempotent — re-running refreshes checked_at. " +
746
- "For a human-verified metadata flag rather than a registration check, use pica_works_verify.",
791
+ "This is a registration check, not a human sign-off — a person's own statement that a human made the work is pica_works_attest (signed-in only).",
747
792
  workflows: ["work-required"],
748
793
  vernacular_kind: "specialist",
749
794
  vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4 substrate writes.",
@@ -857,23 +902,80 @@ export class WorksTools {
857
902
  * Query works — unified search, list, and health filter
858
903
  */
859
904
  async queryWorks(args) {
905
+ // `verified` used to filter rows on `work.is_verified`, a column no
906
+ // applied migration adds to `works`, so it matched nothing whichever way
907
+ // it was set. Refuse
908
+ // it by name rather than ignore it: an agent that asked for "verified
909
+ // works" would otherwise read the whole catalogue as the answer.
910
+ if (args.verified !== undefined) {
911
+ return typedError("NO_WORK_VERIFIED_FLAG", "pica_works_query has no 'verified' filter: PICA does not keep a single 'verified' flag on a work. " +
912
+ "To find works missing what a registration needs, use health_filter: 'low_completeness' or " +
913
+ "completeness_score_lt; to review one work's confirmed and unconfirmed records, use pica_works_inspect.");
914
+ }
860
915
  const limit = args.limit || 50;
861
916
  const offset = args.offset || 0;
862
- // Health filter routes to health/completeness APIs
917
+ // Health filter routes to health/completeness APIs.
918
+ //
919
+ // ⚠️ Both branches used to call the bare SDK method with no arguments,
920
+ // so `limit`/`offset` were ignored (the routes' own default of 20 won)
921
+ // and `total` was the length of that page — an agent asking for 100
922
+ // low-completeness works got 20 and was told there were 20 (ops issue
923
+ // 314325eb, 2026-09-25). Both now thread limit/offset through and report
924
+ // only the totals the server can actually vouch for.
863
925
  if (args.health_filter === "needs_attention") {
864
- const result = await this.pica.health.getWorksHealth();
865
- const works = Array.isArray(result) ? result : result?.works || [];
926
+ // Each candidate is a full cross-org chain assembly, so the route caps
927
+ // a page at 50 candidates; `offset` pages candidates, not results.
928
+ const page = await this.pica.health.getWorksHealthPage({
929
+ limit: Math.min(limit, WORKS_HEALTH_MAX_PAGE),
930
+ offset,
931
+ unreconciledOnly: true,
932
+ });
933
+ const works = page.data;
934
+ const meta = page.meta;
935
+ // No meta (an older server) means nothing is known about the rest of
936
+ // the set: treat it as "more may remain" rather than "done".
937
+ const hasMore = meta ? meta.has_more : works.length > 0;
938
+ // A total is only claimed when THIS call, starting at offset 0, checked
939
+ // every candidate and the server said so (meta.has_more false).
940
+ // Otherwise the page length is not passed off as the total.
941
+ const wholeSetScanned = offset === 0 && meta !== undefined && !hasMore;
866
942
  return formatStructuredList(works, "work", {
867
943
  filter: "needs_attention",
868
- total: works.length,
944
+ total: wholeSetScanned ? works.length : null,
945
+ ...(wholeSetScanned
946
+ ? {}
947
+ : {
948
+ total_note: hasMore
949
+ ? "no total yet — the number needing attention is known only after every candidate work is checked; keep paging with next_offset and count the rows across pages"
950
+ : "this was the last page — the number needing attention is the rows counted across the pages you fetched",
951
+ }),
952
+ limit: meta?.limit ?? Math.min(limit, WORKS_HEALTH_MAX_PAGE),
953
+ offset,
954
+ scanned: meta?.scanned ?? null,
955
+ candidates_total: meta?.candidates_total ?? null,
956
+ hasMore,
957
+ next_offset: meta?.next_offset ?? null,
869
958
  });
870
959
  }
871
960
  if (args.health_filter === "low_completeness") {
872
- const result = await this.pica.health.getLowScoreWorks();
873
- const works = Array.isArray(result) ? result : result?.works || [];
961
+ const page = await this.pica.health.getLowScoreWorksPage({
962
+ limit: Math.min(limit, LOW_COMPLETENESS_MAX_PAGE),
963
+ offset,
964
+ });
965
+ const works = page.data;
966
+ const meta = page.meta;
967
+ const appliedLimit = meta?.limit ?? Math.min(limit, LOW_COMPLETENESS_MAX_PAGE);
968
+ const total = meta?.total ?? null;
969
+ const hasMore = meta?.has_more ??
970
+ (total === null ? works.length >= appliedLimit : false);
874
971
  return formatStructuredList(works, "work", {
875
972
  filter: "low_completeness",
876
- total: works.length,
973
+ threshold: meta?.threshold,
974
+ total,
975
+ limit: appliedLimit,
976
+ offset,
977
+ hasMore,
978
+ next_offset: hasMore ? offset + appliedLimit : null,
877
979
  });
878
980
  }
879
981
  // ADR-244 PR (c) §3.8 / §3.9 — queryable + sortable signals threaded
@@ -930,10 +1032,6 @@ export class WorksTools {
930
1032
  let items = result.data || result;
931
1033
  if (!Array.isArray(items))
932
1034
  items = [];
933
- // Client-side verified filter — preserved from pre-PR (c) behaviour.
934
- if (args.verified !== undefined) {
935
- items = items.filter((work) => work.is_verified === args.verified);
936
- }
937
1035
  // `light` projection — a full works row is ~1KB (completeness components,
938
1036
  // streaming sub-scores, AI disclosure, provenance, album-art URLs), so a
939
1037
  // 100-row page can blow the 100KB MCP response cap. When an agent only
@@ -1008,8 +1106,12 @@ export class WorksTools {
1008
1106
  const allSections = !sections || sections.length === 0;
1009
1107
  // Sections kept OFF the default `allSections` surface but still callable
1010
1108
  // via explicit `sections`:
1011
- // - ADR-201 closeout (2026-04-27): `health` and `score` are org-wide
1012
- // metrics that don't take a workId and cost ~25s / ~2s on staging.
1109
+ // - `health` is a summary of THIS work's completeness (the same
1110
+ // single-work read as `completeness`), so on the default surface it
1111
+ // would only repeat that section. Until 2026-09-26 it read the
1112
+ // ORG-WIDE works-health scan instead — ~25s, and about other works.
1113
+ // - `score` (ADR-201 closeout, 2026-04-27) is the org-wide PICA score;
1114
+ // it takes no workId.
1013
1115
  // - `dossier` (2026-07-12) assembles the whole song (recordings,
1014
1116
  // releases, credits, people, provenance, consent) — a heavy superset of
1015
1117
  // the other sections, redundant on the default surface and worth its
@@ -1103,7 +1205,8 @@ export class WorksTools {
1103
1205
  }
1104
1206
  }
1105
1207
  // ADR-201 closeout (2026-04-27): the section sub-calls used to run
1106
- // sequentially. With ~8 calls each taking ~1s and one (works-health)
1208
+ // sequentially. With ~8 calls each taking ~1s and one (the org-wide
1209
+ // works-health scan `health` read until 2026-09-26)
1107
1210
  // sometimes 25s+, the function would frequently exceed Vercel's 30s
1108
1211
  // maxDuration on the default `allSections` path. Fan out everything
1109
1212
  // in parallel — each branch still owns its own try/catch fallback so
@@ -1145,11 +1248,22 @@ export class WorksTools {
1145
1248
  })());
1146
1249
  }
1147
1250
  }
1148
- // Quality sections — fan out in parallel.
1251
+ // Quality sections — fan out in parallel. `health` and `completeness`
1252
+ // share ONE read of this work's completeness route.
1253
+ let completenessRead = null;
1254
+ const readCompleteness = () => (completenessRead ??= this.pica.health.getWorkCompleteness(workId));
1149
1255
  if (has("health")) {
1150
1256
  sectionTasks.push((async () => {
1151
1257
  try {
1152
- result.health = await this.pica.health.getWorksHealth();
1258
+ const c = await readCompleteness();
1259
+ result.health = {
1260
+ scope: "work",
1261
+ work_id: workId,
1262
+ score: c?.score ?? null,
1263
+ missing_field_count: c?.missing_field_count ?? null,
1264
+ missing_critical: c?.missingCritical ?? [],
1265
+ suggestions: c?.suggestions ?? [],
1266
+ };
1153
1267
  }
1154
1268
  catch (e) {
1155
1269
  result.health = null;
@@ -1160,9 +1274,34 @@ export class WorksTools {
1160
1274
  if (has("completeness")) {
1161
1275
  sectionTasks.push((async () => {
1162
1276
  try {
1163
- const completeness = await this.pica.health.getWorkCompleteness(workId);
1277
+ const completeness = await readCompleteness();
1164
1278
  result.completeness = completeness;
1165
1279
  hints.push(...mapGapsToHints(completeness?.gaps || completeness?.missing || []));
1280
+ // ops_issue 1bc63a6d — only an instrumental/PA is attached. The
1281
+ // candidates (unassigned files whose name matches the title) sit
1282
+ // in completeness.audio.gap; they are for the user to confirm.
1283
+ if (completeness?.audio?.checked === false) {
1284
+ hints.push({
1285
+ gap: "audio_check_unavailable",
1286
+ suggestion: "PICA could not check this work's attached audio just now, so an 'only an instrumental is attached' gap may be missing from this answer.",
1287
+ severity: "nice_to_have",
1288
+ });
1289
+ }
1290
+ const vocalGap = completeness?.audio?.gap;
1291
+ if (vocalGap && typeof vocalGap.label === "string") {
1292
+ const ids = Array.isArray(vocalGap.candidates)
1293
+ ? vocalGap.candidates
1294
+ .map((c) => c.audio_file_id)
1295
+ .filter(Boolean)
1296
+ : [];
1297
+ hints.push({
1298
+ gap: "only_instrumental_attached",
1299
+ suggestion: ids.length > 0
1300
+ ? `${vocalGap.label} (${ids.join(", ")}). If the user confirms one is the vocal master, it can be attached with pica_audio_assign.`
1301
+ : `${vocalGap.label}.`,
1302
+ severity: "important",
1303
+ });
1304
+ }
1166
1305
  }
1167
1306
  catch (e) {
1168
1307
  result.completeness = null;
@@ -1387,7 +1526,7 @@ export class WorksTools {
1387
1526
  artistId: args.primary_artist_id ?? null,
1388
1527
  }, existing);
1389
1528
  if (match)
1390
- versionWarning = buildVersionWarning(match);
1529
+ versionWarning = buildVersionWarning(match, String(work.id));
1391
1530
  }
1392
1531
  }
1393
1532
  catch {
@@ -1426,13 +1565,33 @@ export class WorksTools {
1426
1565
  * Update work
1427
1566
  */
1428
1567
  async updateWork(args) {
1429
- const { id, ...updates } = args;
1568
+ const { verbose, rest } = takeVerbose(args);
1569
+ const { id, ...updates } = rest;
1570
+ // Compact receipt (ops 61ce1dcc): read the gaps open BEFORE the write so
1571
+ // the answer can name only the ones this write opened. Skipped when the
1572
+ // caller asked for the full record, which repeats every gap anyway.
1573
+ const before = verbose
1574
+ ? null
1575
+ : await buildIntegrityPressure(this.pica, "work", id);
1430
1576
  const work = await this.pica.works.update(id, updates);
1431
1577
  // Registration integrity floor (Task 9) — same pressure rendering as
1432
1578
  // createWork, so an update that clears (or introduces) a gap keeps the
1433
1579
  // agent informed without a separate inspect round-trip.
1434
1580
  const pressure = await buildIntegrityPressure(this.pica, "work", work.id);
1435
- return formatSuccess("Work updated successfully", { ...work, integrity: pressure.integrity }, pressure.hints);
1581
+ if (verbose) {
1582
+ return formatSuccess("Work updated successfully", { ...work, integrity: pressure.integrity }, pressure.hints);
1583
+ }
1584
+ const openBefore = before?.integrity
1585
+ ? new Set(before.integrity.gaps.map((g) => g.code))
1586
+ : null;
1587
+ const { changed, unconfirmed } = changedFields(updates, work);
1588
+ return formatSuccess("Work updated successfully", {
1589
+ id: work?.id ?? id,
1590
+ changed,
1591
+ ...(unconfirmed.length > 0 ? { unconfirmed } : {}),
1592
+ integrity: slimIntegrity(pressure.integrity, openBefore),
1593
+ receipt: RECEIPT_NOTE,
1594
+ }, newHintsOnly(before?.integrity ? before.hints : null, pressure.hints));
1436
1595
  }
1437
1596
  /**
1438
1597
  * Delete work
@@ -1442,44 +1601,28 @@ export class WorksTools {
1442
1601
  return formatDeletion("work", args.id, outcome);
1443
1602
  }
1444
1603
  /**
1445
- * Verify work
1604
+ * pica_works_verify — writes nothing, and says so.
1605
+ *
1606
+ * It used to PATCH `is_verified: true` onto the work. `works` has no such
1607
+ * column (no applied migration adds one), so the route's allowed-field check
1608
+ * refused it on every call — its only two calls ever both failed (DonnySavv,
1609
+ * feedback 86e26f75, 2026-09-28). PICA keeps no work-level "metadata
1610
+ * verified" flag, and inventing one would record an agreement nobody made.
1611
+ * What a person CAN confirm lives on specific records, each refused to
1612
+ * agents and each done by the person on /inspect — this names them.
1446
1613
  */
1447
1614
  async verifyWork(args) {
1448
- const work = await this.pica.works.verify(args.id);
1449
- // ADR-146: Also check export readiness
1450
- let exportReadiness;
1451
- try {
1452
- const baseUrl = this.pica.works["baseUrl"];
1453
- const apiKey = this.pica.works["apiKey"];
1454
- const res = await fetch(`${baseUrl}/admin/export-readiness/${args.id}`, {
1455
- headers: {
1456
- Authorization: `Bearer ${apiKey}`,
1457
- "Content-Type": "application/json",
1458
- },
1459
- });
1460
- if (res.ok) {
1461
- const json = (await res.json());
1462
- exportReadiness = json.data ?? json;
1463
- }
1464
- }
1465
- catch {
1466
- // Readiness check is best-effort — don't fail verification
1467
- }
1468
- const summaryParts = ["Work verified successfully"];
1469
- if (exportReadiness) {
1470
- summaryParts.push(`Export readiness: ${exportReadiness.ready ? "ready" : "not ready"}`, `Evidence: ${exportReadiness.evidenceStrength}`, `Attestation: ${exportReadiness.attestationCoverage}%`);
1471
- if (Array.isArray(exportReadiness.blockers) &&
1472
- exportReadiness.blockers.length > 0) {
1473
- summaryParts.push(`Blockers (${exportReadiness.blockers.length}):`);
1474
- for (const b of exportReadiness.blockers) {
1475
- summaryParts.push(` ${b.type}: ${b.resolutionHint}`);
1476
- }
1477
- }
1615
+ const workId = typeof args.id === "string" ? args.id.trim() : "";
1616
+ if (!workId) {
1617
+ return typedError("VALIDATION_ERROR", "id is required.");
1478
1618
  }
1479
- return formatSuccess(summaryParts.join("\n"), {
1480
- ...work,
1481
- exportReadiness,
1482
- });
1619
+ return typedError("NO_WORK_VERIFIED_FLAG", "PICA does not keep a single 'verified' flag on a work, so nothing was written. " +
1620
+ "What a person can confirm, each while signed in to PICA (an assistant cannot do these for them): " +
1621
+ `(1) the human-made stamp — "stamp as human-made" at /inspect/works/${workId}#human-made; ` +
1622
+ `(2) their own writer credit — confirm it in the credits table on /inspect/works/${workId}; ` +
1623
+ '(3) their own master ownership share — "confirm my share" under ownership splits at ' +
1624
+ "/inspect/recordings/<recording_id>#ownership-splits. " +
1625
+ "To review what is missing or unconfirmed on the work, use pica_works_inspect.");
1483
1626
  }
1484
1627
  /**
1485
1628
  * Bulk delete works