@withpica/mcp-server 2.97.5 → 2.98.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 (149) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/README.intro.md +1 -1
  3. package/README.md +7 -3
  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 +23 -0
  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 +17 -20
  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/required-schemas.generated.d.ts +7 -5
  18. package/dist/resources/required-schemas.generated.d.ts.map +1 -1
  19. package/dist/resources/required-schemas.generated.js +14 -2
  20. package/dist/resources/required-schemas.generated.js.map +1 -1
  21. package/dist/resources/required-schemas.source.d.ts.map +1 -1
  22. package/dist/resources/required-schemas.source.js +8 -1
  23. package/dist/resources/required-schemas.source.js.map +1 -1
  24. package/dist/server-instructions.d.ts +2 -2
  25. package/dist/server-instructions.d.ts.map +1 -1
  26. package/dist/server-instructions.js +1 -1
  27. package/dist/skills/skills.generated.js +5 -5
  28. package/dist/skills/skills.generated.js.map +1 -1
  29. package/dist/tools/agreement-types.d.ts.map +1 -1
  30. package/dist/tools/agreement-types.js +3 -1
  31. package/dist/tools/agreement-types.js.map +1 -1
  32. package/dist/tools/agreements.d.ts.map +1 -1
  33. package/dist/tools/agreements.js +15 -6
  34. package/dist/tools/agreements.js.map +1 -1
  35. package/dist/tools/approvals.d.ts.map +1 -1
  36. package/dist/tools/approvals.js +14 -1
  37. package/dist/tools/approvals.js.map +1 -1
  38. package/dist/tools/arg-aliases.d.ts.map +1 -1
  39. package/dist/tools/arg-aliases.js +12 -0
  40. package/dist/tools/arg-aliases.js.map +1 -1
  41. package/dist/tools/audio-files.d.ts +39 -0
  42. package/dist/tools/audio-files.d.ts.map +1 -1
  43. package/dist/tools/audio-files.js +253 -12
  44. package/dist/tools/audio-files.js.map +1 -1
  45. package/dist/tools/catalog-enums.d.ts +20 -0
  46. package/dist/tools/catalog-enums.d.ts.map +1 -0
  47. package/dist/tools/catalog-enums.js +28 -0
  48. package/dist/tools/catalog-enums.js.map +1 -0
  49. package/dist/tools/credits.d.ts.map +1 -1
  50. package/dist/tools/credits.js +64 -4
  51. package/dist/tools/credits.js.map +1 -1
  52. package/dist/tools/custody-hints.js +2 -2
  53. package/dist/tools/custody-hints.js.map +1 -1
  54. package/dist/tools/custody.d.ts.map +1 -1
  55. package/dist/tools/custody.js +4 -1
  56. package/dist/tools/custody.js.map +1 -1
  57. package/dist/tools/deprecations.d.ts +2 -1
  58. package/dist/tools/deprecations.d.ts.map +1 -1
  59. package/dist/tools/deprecations.js +27 -11
  60. package/dist/tools/deprecations.js.map +1 -1
  61. package/dist/tools/directory.js +2 -2
  62. package/dist/tools/directory.js.map +1 -1
  63. package/dist/tools/discovery.d.ts.map +1 -1
  64. package/dist/tools/discovery.js +13 -3
  65. package/dist/tools/discovery.js.map +1 -1
  66. package/dist/tools/duplicates.d.ts.map +1 -1
  67. package/dist/tools/duplicates.js +66 -18
  68. package/dist/tools/duplicates.js.map +1 -1
  69. package/dist/tools/enrichment.d.ts.map +1 -1
  70. package/dist/tools/enrichment.js +36 -2
  71. package/dist/tools/enrichment.js.map +1 -1
  72. package/dist/tools/import.d.ts.map +1 -1
  73. package/dist/tools/import.js +19 -0
  74. package/dist/tools/import.js.map +1 -1
  75. package/dist/tools/index.d.ts.map +1 -1
  76. package/dist/tools/index.js +44 -4
  77. package/dist/tools/index.js.map +1 -1
  78. package/dist/tools/licensing.d.ts.map +1 -1
  79. package/dist/tools/licensing.js +14 -9
  80. package/dist/tools/licensing.js.map +1 -1
  81. package/dist/tools/metadata.d.ts.map +1 -1
  82. package/dist/tools/metadata.js +52 -12
  83. package/dist/tools/metadata.js.map +1 -1
  84. package/dist/tools/notes.d.ts.map +1 -1
  85. package/dist/tools/notes.js +28 -5
  86. package/dist/tools/notes.js.map +1 -1
  87. package/dist/tools/people.d.ts +5 -1
  88. package/dist/tools/people.d.ts.map +1 -1
  89. package/dist/tools/people.js +8 -1
  90. package/dist/tools/people.js.map +1 -1
  91. package/dist/tools/purchases.d.ts +2 -3
  92. package/dist/tools/purchases.d.ts.map +1 -1
  93. package/dist/tools/purchases.js.map +1 -1
  94. package/dist/tools/recording-attribution-hints.js +1 -1
  95. package/dist/tools/recording-attribution-hints.js.map +1 -1
  96. package/dist/tools/recording-custody.d.ts +3 -1
  97. package/dist/tools/recording-custody.d.ts.map +1 -1
  98. package/dist/tools/recording-custody.js +8 -4
  99. package/dist/tools/recording-custody.js.map +1 -1
  100. package/dist/tools/recovery-hints.d.ts +0 -6
  101. package/dist/tools/recovery-hints.d.ts.map +1 -1
  102. package/dist/tools/recovery-hints.js +73 -15
  103. package/dist/tools/recovery-hints.js.map +1 -1
  104. package/dist/tools/register.d.ts.map +1 -1
  105. package/dist/tools/register.js +2 -1
  106. package/dist/tools/register.js.map +1 -1
  107. package/dist/tools/releases.d.ts.map +1 -1
  108. package/dist/tools/releases.js +7 -4
  109. package/dist/tools/releases.js.map +1 -1
  110. package/dist/tools/rights.d.ts +45 -0
  111. package/dist/tools/rights.d.ts.map +1 -0
  112. package/dist/tools/rights.js +302 -0
  113. package/dist/tools/rights.js.map +1 -0
  114. package/dist/tools/royalties.d.ts +24 -1
  115. package/dist/tools/royalties.d.ts.map +1 -1
  116. package/dist/tools/royalties.js +99 -66
  117. package/dist/tools/royalties.js.map +1 -1
  118. package/dist/tools/section-validation.d.ts +14 -0
  119. package/dist/tools/section-validation.d.ts.map +1 -0
  120. package/dist/tools/section-validation.js +68 -0
  121. package/dist/tools/section-validation.js.map +1 -0
  122. package/dist/tools/selections.d.ts +5 -2
  123. package/dist/tools/selections.d.ts.map +1 -1
  124. package/dist/tools/selections.js +10 -4
  125. package/dist/tools/selections.js.map +1 -1
  126. package/dist/tools/share-links.d.ts.map +1 -1
  127. package/dist/tools/share-links.js +32 -6
  128. package/dist/tools/share-links.js.map +1 -1
  129. package/dist/tools/share-send.d.ts.map +1 -1
  130. package/dist/tools/share-send.js +32 -7
  131. package/dist/tools/share-send.js.map +1 -1
  132. package/dist/tools/signed-split-sheet-preview.d.ts +43 -0
  133. package/dist/tools/signed-split-sheet-preview.d.ts.map +1 -0
  134. package/dist/tools/signed-split-sheet-preview.js +67 -0
  135. package/dist/tools/signed-split-sheet-preview.js.map +1 -0
  136. package/dist/tools/split-sheets.d.ts.map +1 -1
  137. package/dist/tools/split-sheets.js +3 -2
  138. package/dist/tools/split-sheets.js.map +1 -1
  139. package/dist/tools/works.d.ts +9 -1
  140. package/dist/tools/works.d.ts.map +1 -1
  141. package/dist/tools/works.js +194 -64
  142. package/dist/tools/works.js.map +1 -1
  143. package/dist/tools/write-receipt.d.ts +68 -0
  144. package/dist/tools/write-receipt.d.ts.map +1 -0
  145. package/dist/tools/write-receipt.js +64 -0
  146. package/dist/tools/write-receipt.js.map +1 -0
  147. package/package.json +4 -4
  148. package/scripts/build-required-schemas.ts +7 -1
  149. 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,15 @@ 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
+ },
123
135
  display_artist: {
124
136
  type: "string",
125
137
  description: "Display-friendly artist string (e.g. 'Jane Doe ft. John Smith')",
@@ -142,7 +154,7 @@ export const WORK_WRITE_PROPERTIES = {
142
154
  },
143
155
  notes: {
144
156
  type: "string",
145
- description: "Internal notes",
157
+ description: "Internal notes. Setting this REPLACES the whole field — to add a line, use append_notes on pica_works_update instead.",
146
158
  },
147
159
  metadata: {
148
160
  type: "object",
@@ -200,6 +212,14 @@ function summariseDossier(dossier) {
200
212
  `Consent: ${consentState}${consentLevel}. Completeness: ${completeness}. ` +
201
213
  `(Full payload under structuredContent.data.dossier; PDF / multi-work ZIP: pica_export_song_dossiers.)`);
202
214
  }
215
+ /**
216
+ * Page caps for the two health_filter branches of pica_works_query — the
217
+ * routes' own maxima (app/api/admin/quality/works-health/route.ts and
218
+ * .../completeness/low-score/route.ts). Hand-copied (this package cannot
219
+ * import the app); no test ties them — change both together.
220
+ */
221
+ const WORKS_HEALTH_MAX_PAGE = 50;
222
+ const LOW_COMPLETENESS_MAX_PAGE = 200;
203
223
  export class WorksTools {
204
224
  pica;
205
225
  constructor(pica) {
@@ -247,7 +267,7 @@ export class WorksTools {
247
267
  properties: {
248
268
  query: {
249
269
  type: "string",
250
- description: "Search by title, ISWC, or artist name",
270
+ 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).",
251
271
  },
252
272
  verified: {
253
273
  type: "boolean",
@@ -256,7 +276,7 @@ export class WorksTools {
256
276
  health_filter: {
257
277
  type: "string",
258
278
  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.)",
279
+ 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
280
  },
261
281
  has_statement_data: {
262
282
  type: "boolean",
@@ -304,7 +324,7 @@ export class WorksTools {
304
324
  },
305
325
  work_type: {
306
326
  type: "string",
307
- enum: ["song", "instrumental", "library", "demo", "sample"],
327
+ enum: [...WORK_TYPE_VALUES],
308
328
  description: "Filter by work type. 'instrumental' answers 'my instrumentals'.",
309
329
  },
310
330
  work_status: {
@@ -337,11 +357,11 @@ export class WorksTools {
337
357
  },
338
358
  primary_artist: {
339
359
  type: "string",
340
- description: "Exact primary-artist name match.",
360
+ 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
361
  },
342
362
  artist: {
343
363
  type: "string",
344
- description: "Exact primary-artist name match (alias).",
364
+ 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
365
  },
346
366
  label: {
347
367
  type: "string",
@@ -468,7 +488,7 @@ export class WorksTools {
468
488
  "dossier",
469
489
  ],
470
490
  },
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.',
491
+ 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
492
  },
473
493
  },
474
494
  required: [],
@@ -515,7 +535,9 @@ export class WorksTools {
515
535
  "mastering human-made, AI-assisted, or AI-generated? (Those four " +
516
536
  "are set independently on ai_disclosure.) Always run " +
517
537
  "pica_works_query first to check for a duplicate — two same-title " +
518
- "works cause split and registration trouble. Album, artwork, " +
538
+ "works cause split and registration trouble. If a refusal says the " +
539
+ "work already exists but it really is a different song with the same " +
540
+ "title and artist, retry with disambiguation (e.g. 'theme'). Album, artwork, " +
519
541
  "track number, UPC, and Spotify URL live on the release " +
520
542
  "(pica_releases_create/update), not here; label too, via the " +
521
543
  "release (resolve the label name with pica_labels_query first). " +
@@ -570,6 +592,7 @@ export class WorksTools {
570
592
  "MCP; set them through a release or recording path. " +
571
593
  "Three jsonb compliance fields (ai_disclosure, provenance_attestation, training_rights) route " +
572
594
  "to that same work_licensing satellite table and ARE set-able here. " +
595
+ "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
596
  "→ then: pica_works_inspect (verify changes persisted)",
574
597
  workflows: ["work-required"],
575
598
  vernacular_kind: "specialist",
@@ -582,6 +605,14 @@ export class WorksTools {
582
605
  description: "Work ID (required)",
583
606
  },
584
607
  ...WORK_WRITE_PROPERTIES,
608
+ ...VERBOSE_PROPERTY,
609
+ append_notes: {
610
+ type: "string",
611
+ maxLength: 10000,
612
+ description: "Text to add to the end of the work's notes, on a new line, without reading or rewriting what is there. " +
613
+ "Safe when someone else edits the work at the same time (a conflict is retried, never overwritten). " +
614
+ "Cannot be sent together with notes.",
615
+ },
585
616
  },
586
617
  required: ["id"],
587
618
  },
@@ -609,7 +640,7 @@ export class WorksTools {
609
640
  description: "Delete a musical work. It is automatically unlinked from any " +
610
641
  "releases it appears on (the release keeps its track). If the work " +
611
642
  "still has dependent records (recordings, royalty payments, version " +
612
- "links, claims) it is archived instead — hidden from the catalog but " +
643
+ "links, claims, signed split sheets) it is archived instead — hidden from the catalog but " +
613
644
  "preserved — and the result tells you exactly what to remove to " +
614
645
  "delete it permanently. A work with registry identifiers " +
615
646
  "(ISRC/ISWC/MLC) is always archived, never destroyed. The result " +
@@ -637,8 +668,11 @@ export class WorksTools {
637
668
  scope: "write:catalog",
638
669
  audiences: ["stdio", "http", "public"],
639
670
  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).",
671
+ description: "Writes nothing: PICA keeps no single 'verified' flag on a work. Returns " +
672
+ "NO_WORK_VERIFIED_FLAG naming what the person can confirm themselves, signed in, " +
673
+ "on /inspect — the human-made stamp, their own writer credit, their own master " +
674
+ "share. Not a registration check (that's pica_works_verify_registrations). To " +
675
+ "review a work, use pica_works_inspect.",
642
676
  workflows: "infrastructure",
643
677
  vernacular_kind: "specialist",
644
678
  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 +703,11 @@ export class WorksTools {
669
703
  "how, and an optional signature hash (the raw signature itself is " +
670
704
  "never stored). Sync libraries and provenance certificates use it " +
671
705
  "to prove human authorship. To change or undo one, use the " +
672
- "provenance revocation tools. After stamping, read it back with " +
706
+ "provenance revocation tools. Only a signed-in person can make " +
707
+ "this statement: an API key, connected app or agent is refused " +
708
+ "with ATTESTATION_REQUIRES_HUMAN_SESSION — do not retry; tell the " +
709
+ "user to use 'stamp as human-made' at /inspect/works/<id>#human-made. " +
710
+ "After stamping, read it back with " +
673
711
  "pica_works_inspect (sections: ['provenance']). → then: " +
674
712
  "pica_works_inspect (verify it persisted), " +
675
713
  "pica_provenance_certificate (generate a PDF)",
@@ -743,7 +781,7 @@ export class WorksTools {
743
781
  "mode 'confirm_unregistered' records the user's own confirmation instead (no lookup; " +
744
782
  "society_code optional — omitted means 'not registered with any society'). " +
745
783
  "Idempotent — re-running refreshes checked_at. " +
746
- "For a human-verified metadata flag rather than a registration check, use pica_works_verify.",
784
+ "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
785
  workflows: ["work-required"],
748
786
  vernacular_kind: "specialist",
749
787
  vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4 substrate writes.",
@@ -859,21 +897,68 @@ export class WorksTools {
859
897
  async queryWorks(args) {
860
898
  const limit = args.limit || 50;
861
899
  const offset = args.offset || 0;
862
- // Health filter routes to health/completeness APIs
900
+ // Health filter routes to health/completeness APIs.
901
+ //
902
+ // ⚠️ Both branches used to call the bare SDK method with no arguments,
903
+ // so `limit`/`offset` were ignored (the routes' own default of 20 won)
904
+ // and `total` was the length of that page — an agent asking for 100
905
+ // low-completeness works got 20 and was told there were 20 (ops issue
906
+ // 314325eb, 2026-09-25). Both now thread limit/offset through and report
907
+ // only the totals the server can actually vouch for.
863
908
  if (args.health_filter === "needs_attention") {
864
- const result = await this.pica.health.getWorksHealth();
865
- const works = Array.isArray(result) ? result : result?.works || [];
909
+ // Each candidate is a full cross-org chain assembly, so the route caps
910
+ // a page at 50 candidates; `offset` pages candidates, not results.
911
+ const page = await this.pica.health.getWorksHealthPage({
912
+ limit: Math.min(limit, WORKS_HEALTH_MAX_PAGE),
913
+ offset,
914
+ unreconciledOnly: true,
915
+ });
916
+ const works = page.data;
917
+ const meta = page.meta;
918
+ // No meta (an older server) means nothing is known about the rest of
919
+ // the set: treat it as "more may remain" rather than "done".
920
+ const hasMore = meta ? meta.has_more : works.length > 0;
921
+ // A total is only claimed when THIS call, starting at offset 0, checked
922
+ // every candidate and the server said so (meta.has_more false).
923
+ // Otherwise the page length is not passed off as the total.
924
+ const wholeSetScanned = offset === 0 && meta !== undefined && !hasMore;
866
925
  return formatStructuredList(works, "work", {
867
926
  filter: "needs_attention",
868
- total: works.length,
927
+ total: wholeSetScanned ? works.length : null,
928
+ ...(wholeSetScanned
929
+ ? {}
930
+ : {
931
+ total_note: hasMore
932
+ ? "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"
933
+ : "this was the last page — the number needing attention is the rows counted across the pages you fetched",
934
+ }),
935
+ limit: meta?.limit ?? Math.min(limit, WORKS_HEALTH_MAX_PAGE),
936
+ offset,
937
+ scanned: meta?.scanned ?? null,
938
+ candidates_total: meta?.candidates_total ?? null,
939
+ hasMore,
940
+ next_offset: meta?.next_offset ?? null,
869
941
  });
870
942
  }
871
943
  if (args.health_filter === "low_completeness") {
872
- const result = await this.pica.health.getLowScoreWorks();
873
- const works = Array.isArray(result) ? result : result?.works || [];
944
+ const page = await this.pica.health.getLowScoreWorksPage({
945
+ limit: Math.min(limit, LOW_COMPLETENESS_MAX_PAGE),
946
+ offset,
947
+ });
948
+ const works = page.data;
949
+ const meta = page.meta;
950
+ const appliedLimit = meta?.limit ?? Math.min(limit, LOW_COMPLETENESS_MAX_PAGE);
951
+ const total = meta?.total ?? null;
952
+ const hasMore = meta?.has_more ??
953
+ (total === null ? works.length >= appliedLimit : false);
874
954
  return formatStructuredList(works, "work", {
875
955
  filter: "low_completeness",
876
- total: works.length,
956
+ threshold: meta?.threshold,
957
+ total,
958
+ limit: appliedLimit,
959
+ offset,
960
+ hasMore,
961
+ next_offset: hasMore ? offset + appliedLimit : null,
877
962
  });
878
963
  }
879
964
  // ADR-244 PR (c) §3.8 / §3.9 — queryable + sortable signals threaded
@@ -1008,8 +1093,12 @@ export class WorksTools {
1008
1093
  const allSections = !sections || sections.length === 0;
1009
1094
  // Sections kept OFF the default `allSections` surface but still callable
1010
1095
  // 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.
1096
+ // - `health` is a summary of THIS work's completeness (the same
1097
+ // single-work read as `completeness`), so on the default surface it
1098
+ // would only repeat that section. Until 2026-09-26 it read the
1099
+ // ORG-WIDE works-health scan instead — ~25s, and about other works.
1100
+ // - `score` (ADR-201 closeout, 2026-04-27) is the org-wide PICA score;
1101
+ // it takes no workId.
1013
1102
  // - `dossier` (2026-07-12) assembles the whole song (recordings,
1014
1103
  // releases, credits, people, provenance, consent) — a heavy superset of
1015
1104
  // the other sections, redundant on the default surface and worth its
@@ -1103,7 +1192,8 @@ export class WorksTools {
1103
1192
  }
1104
1193
  }
1105
1194
  // 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)
1195
+ // sequentially. With ~8 calls each taking ~1s and one (the org-wide
1196
+ // works-health scan `health` read until 2026-09-26)
1107
1197
  // sometimes 25s+, the function would frequently exceed Vercel's 30s
1108
1198
  // maxDuration on the default `allSections` path. Fan out everything
1109
1199
  // in parallel — each branch still owns its own try/catch fallback so
@@ -1145,11 +1235,22 @@ export class WorksTools {
1145
1235
  })());
1146
1236
  }
1147
1237
  }
1148
- // Quality sections — fan out in parallel.
1238
+ // Quality sections — fan out in parallel. `health` and `completeness`
1239
+ // share ONE read of this work's completeness route.
1240
+ let completenessRead = null;
1241
+ const readCompleteness = () => (completenessRead ??= this.pica.health.getWorkCompleteness(workId));
1149
1242
  if (has("health")) {
1150
1243
  sectionTasks.push((async () => {
1151
1244
  try {
1152
- result.health = await this.pica.health.getWorksHealth();
1245
+ const c = await readCompleteness();
1246
+ result.health = {
1247
+ scope: "work",
1248
+ work_id: workId,
1249
+ score: c?.score ?? null,
1250
+ missing_field_count: c?.missing_field_count ?? null,
1251
+ missing_critical: c?.missingCritical ?? [],
1252
+ suggestions: c?.suggestions ?? [],
1253
+ };
1153
1254
  }
1154
1255
  catch (e) {
1155
1256
  result.health = null;
@@ -1160,9 +1261,34 @@ export class WorksTools {
1160
1261
  if (has("completeness")) {
1161
1262
  sectionTasks.push((async () => {
1162
1263
  try {
1163
- const completeness = await this.pica.health.getWorkCompleteness(workId);
1264
+ const completeness = await readCompleteness();
1164
1265
  result.completeness = completeness;
1165
1266
  hints.push(...mapGapsToHints(completeness?.gaps || completeness?.missing || []));
1267
+ // ops_issue 1bc63a6d — only an instrumental/PA is attached. The
1268
+ // candidates (unassigned files whose name matches the title) sit
1269
+ // in completeness.audio.gap; they are for the user to confirm.
1270
+ if (completeness?.audio?.checked === false) {
1271
+ hints.push({
1272
+ gap: "audio_check_unavailable",
1273
+ 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.",
1274
+ severity: "nice_to_have",
1275
+ });
1276
+ }
1277
+ const vocalGap = completeness?.audio?.gap;
1278
+ if (vocalGap && typeof vocalGap.label === "string") {
1279
+ const ids = Array.isArray(vocalGap.candidates)
1280
+ ? vocalGap.candidates
1281
+ .map((c) => c.audio_file_id)
1282
+ .filter(Boolean)
1283
+ : [];
1284
+ hints.push({
1285
+ gap: "only_instrumental_attached",
1286
+ suggestion: ids.length > 0
1287
+ ? `${vocalGap.label} (${ids.join(", ")}). If the user confirms one is the vocal master, it can be attached with pica_audio_assign.`
1288
+ : `${vocalGap.label}.`,
1289
+ severity: "important",
1290
+ });
1291
+ }
1166
1292
  }
1167
1293
  catch (e) {
1168
1294
  result.completeness = null;
@@ -1426,13 +1552,33 @@ export class WorksTools {
1426
1552
  * Update work
1427
1553
  */
1428
1554
  async updateWork(args) {
1429
- const { id, ...updates } = args;
1555
+ const { verbose, rest } = takeVerbose(args);
1556
+ const { id, ...updates } = rest;
1557
+ // Compact receipt (ops 61ce1dcc): read the gaps open BEFORE the write so
1558
+ // the answer can name only the ones this write opened. Skipped when the
1559
+ // caller asked for the full record, which repeats every gap anyway.
1560
+ const before = verbose
1561
+ ? null
1562
+ : await buildIntegrityPressure(this.pica, "work", id);
1430
1563
  const work = await this.pica.works.update(id, updates);
1431
1564
  // Registration integrity floor (Task 9) — same pressure rendering as
1432
1565
  // createWork, so an update that clears (or introduces) a gap keeps the
1433
1566
  // agent informed without a separate inspect round-trip.
1434
1567
  const pressure = await buildIntegrityPressure(this.pica, "work", work.id);
1435
- return formatSuccess("Work updated successfully", { ...work, integrity: pressure.integrity }, pressure.hints);
1568
+ if (verbose) {
1569
+ return formatSuccess("Work updated successfully", { ...work, integrity: pressure.integrity }, pressure.hints);
1570
+ }
1571
+ const openBefore = before?.integrity
1572
+ ? new Set(before.integrity.gaps.map((g) => g.code))
1573
+ : null;
1574
+ const { changed, unconfirmed } = changedFields(updates, work);
1575
+ return formatSuccess("Work updated successfully", {
1576
+ id: work?.id ?? id,
1577
+ changed,
1578
+ ...(unconfirmed.length > 0 ? { unconfirmed } : {}),
1579
+ integrity: slimIntegrity(pressure.integrity, openBefore),
1580
+ receipt: RECEIPT_NOTE,
1581
+ }, newHintsOnly(before?.integrity ? before.hints : null, pressure.hints));
1436
1582
  }
1437
1583
  /**
1438
1584
  * Delete work
@@ -1442,44 +1588,28 @@ export class WorksTools {
1442
1588
  return formatDeletion("work", args.id, outcome);
1443
1589
  }
1444
1590
  /**
1445
- * Verify work
1591
+ * pica_works_verify — writes nothing, and says so.
1592
+ *
1593
+ * It used to PATCH `is_verified: true` onto the work. `works` has no such
1594
+ * column (no applied migration adds one), so the route's allowed-field check
1595
+ * refused it on every call — its only two calls ever both failed (DonnySavv,
1596
+ * feedback 86e26f75, 2026-09-28). PICA keeps no work-level "metadata
1597
+ * verified" flag, and inventing one would record an agreement nobody made.
1598
+ * What a person CAN confirm lives on specific records, each refused to
1599
+ * agents and each done by the person on /inspect — this names them.
1446
1600
  */
1447
1601
  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
- }
1602
+ const workId = typeof args.id === "string" ? args.id.trim() : "";
1603
+ if (!workId) {
1604
+ return typedError("VALIDATION_ERROR", "id is required.");
1478
1605
  }
1479
- return formatSuccess(summaryParts.join("\n"), {
1480
- ...work,
1481
- exportReadiness,
1482
- });
1606
+ return typedError("NO_WORK_VERIFIED_FLAG", "PICA does not keep a single 'verified' flag on a work, so nothing was written. " +
1607
+ "What a person can confirm, each while signed in to PICA (an assistant cannot do these for them): " +
1608
+ `(1) the human-made stamp — "stamp as human-made" at /inspect/works/${workId}#human-made; ` +
1609
+ `(2) their own writer credit — confirm it in the credits table on /inspect/works/${workId}; ` +
1610
+ '(3) their own master ownership share — "confirm my share" under ownership splits at ' +
1611
+ "/inspect/recordings/<recording_id>#ownership-splits. " +
1612
+ "To review what is missing or unconfirmed on the work, use pica_works_inspect.");
1483
1613
  }
1484
1614
  /**
1485
1615
  * Bulk delete works