@withpica/mcp-server 2.97.4 → 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.
- package/CHANGELOG.md +152 -0
- package/README.intro.md +1 -1
- package/README.md +9 -5
- package/dist/lib/changelog.generated.d.ts +2 -2
- package/dist/lib/changelog.generated.d.ts.map +1 -1
- package/dist/lib/changelog.generated.js +2 -2
- package/dist/lib/changelog.generated.js.map +1 -1
- package/dist/prompts/creator-question-atlas.d.ts.map +1 -1
- package/dist/prompts/creator-question-atlas.js +23 -0
- package/dist/prompts/creator-question-atlas.js.map +1 -1
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +22 -25
- package/dist/prompts/index.js.map +1 -1
- package/dist/resources/agent-guide.d.ts.map +1 -1
- package/dist/resources/agent-guide.js +4 -2
- package/dist/resources/agent-guide.js.map +1 -1
- package/dist/resources/required-schemas.generated.d.ts +13 -7
- package/dist/resources/required-schemas.generated.d.ts.map +1 -1
- package/dist/resources/required-schemas.generated.js +23 -4
- package/dist/resources/required-schemas.generated.js.map +1 -1
- package/dist/resources/required-schemas.source.d.ts.map +1 -1
- package/dist/resources/required-schemas.source.js +15 -3
- package/dist/resources/required-schemas.source.js.map +1 -1
- package/dist/server-instructions.d.ts +2 -2
- package/dist/server-instructions.d.ts.map +1 -1
- package/dist/server-instructions.js +1 -1
- package/dist/skills/skills.generated.js +5 -5
- package/dist/skills/skills.generated.js.map +1 -1
- package/dist/tools/agreement-types.d.ts.map +1 -1
- package/dist/tools/agreement-types.js +3 -1
- package/dist/tools/agreement-types.js.map +1 -1
- package/dist/tools/agreements.d.ts.map +1 -1
- package/dist/tools/agreements.js +15 -6
- package/dist/tools/agreements.js.map +1 -1
- package/dist/tools/approvals.d.ts.map +1 -1
- package/dist/tools/approvals.js +14 -1
- package/dist/tools/approvals.js.map +1 -1
- package/dist/tools/arg-aliases.d.ts.map +1 -1
- package/dist/tools/arg-aliases.js +12 -0
- package/dist/tools/arg-aliases.js.map +1 -1
- package/dist/tools/audio-files.d.ts +39 -0
- package/dist/tools/audio-files.d.ts.map +1 -1
- package/dist/tools/audio-files.js +253 -12
- package/dist/tools/audio-files.js.map +1 -1
- package/dist/tools/catalog-enums.d.ts +20 -0
- package/dist/tools/catalog-enums.d.ts.map +1 -0
- package/dist/tools/catalog-enums.js +28 -0
- package/dist/tools/catalog-enums.js.map +1 -0
- package/dist/tools/credits.d.ts.map +1 -1
- package/dist/tools/credits.js +69 -8
- package/dist/tools/credits.js.map +1 -1
- package/dist/tools/custody-hints.js +2 -2
- package/dist/tools/custody-hints.js.map +1 -1
- package/dist/tools/custody.d.ts.map +1 -1
- package/dist/tools/custody.js +4 -1
- package/dist/tools/custody.js.map +1 -1
- package/dist/tools/deprecations.d.ts +2 -1
- package/dist/tools/deprecations.d.ts.map +1 -1
- package/dist/tools/deprecations.js +27 -11
- package/dist/tools/deprecations.js.map +1 -1
- package/dist/tools/directory.js +2 -2
- package/dist/tools/directory.js.map +1 -1
- package/dist/tools/discovery.d.ts.map +1 -1
- package/dist/tools/discovery.js +14 -3
- package/dist/tools/discovery.js.map +1 -1
- package/dist/tools/duplicates.d.ts.map +1 -1
- package/dist/tools/duplicates.js +66 -18
- package/dist/tools/duplicates.js.map +1 -1
- package/dist/tools/enrichment.d.ts.map +1 -1
- package/dist/tools/enrichment.js +36 -2
- package/dist/tools/enrichment.js.map +1 -1
- package/dist/tools/exports.d.ts +1 -0
- package/dist/tools/exports.d.ts.map +1 -1
- package/dist/tools/exports.js +69 -2
- package/dist/tools/exports.js.map +1 -1
- package/dist/tools/import.d.ts.map +1 -1
- package/dist/tools/import.js +19 -0
- package/dist/tools/import.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +44 -4
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/licensing.d.ts.map +1 -1
- package/dist/tools/licensing.js +14 -9
- package/dist/tools/licensing.js.map +1 -1
- package/dist/tools/metadata.d.ts.map +1 -1
- package/dist/tools/metadata.js +58 -12
- package/dist/tools/metadata.js.map +1 -1
- package/dist/tools/notes.d.ts.map +1 -1
- package/dist/tools/notes.js +28 -5
- package/dist/tools/notes.js.map +1 -1
- package/dist/tools/people.d.ts +5 -1
- package/dist/tools/people.d.ts.map +1 -1
- package/dist/tools/people.js +8 -1
- package/dist/tools/people.js.map +1 -1
- package/dist/tools/purchases.d.ts +2 -3
- package/dist/tools/purchases.d.ts.map +1 -1
- package/dist/tools/purchases.js.map +1 -1
- package/dist/tools/recording-attribution-hints.js +1 -1
- package/dist/tools/recording-attribution-hints.js.map +1 -1
- package/dist/tools/recording-custody.d.ts +3 -1
- package/dist/tools/recording-custody.d.ts.map +1 -1
- package/dist/tools/recording-custody.js +8 -4
- package/dist/tools/recording-custody.js.map +1 -1
- package/dist/tools/recovery-hints.d.ts +0 -6
- package/dist/tools/recovery-hints.d.ts.map +1 -1
- package/dist/tools/recovery-hints.js +98 -20
- package/dist/tools/recovery-hints.js.map +1 -1
- package/dist/tools/register.d.ts.map +1 -1
- package/dist/tools/register.js +2 -1
- package/dist/tools/register.js.map +1 -1
- package/dist/tools/releases.d.ts.map +1 -1
- package/dist/tools/releases.js +7 -4
- package/dist/tools/releases.js.map +1 -1
- package/dist/tools/rights.d.ts +45 -0
- package/dist/tools/rights.d.ts.map +1 -0
- package/dist/tools/rights.js +302 -0
- package/dist/tools/rights.js.map +1 -0
- package/dist/tools/royalties.d.ts +24 -1
- package/dist/tools/royalties.d.ts.map +1 -1
- package/dist/tools/royalties.js +99 -66
- package/dist/tools/royalties.js.map +1 -1
- package/dist/tools/section-validation.d.ts +14 -0
- package/dist/tools/section-validation.d.ts.map +1 -0
- package/dist/tools/section-validation.js +68 -0
- package/dist/tools/section-validation.js.map +1 -0
- package/dist/tools/selections.d.ts +5 -2
- package/dist/tools/selections.d.ts.map +1 -1
- package/dist/tools/selections.js +10 -4
- package/dist/tools/selections.js.map +1 -1
- package/dist/tools/settings.d.ts.map +1 -1
- package/dist/tools/settings.js +11 -7
- package/dist/tools/settings.js.map +1 -1
- package/dist/tools/share-links.d.ts.map +1 -1
- package/dist/tools/share-links.js +32 -6
- package/dist/tools/share-links.js.map +1 -1
- package/dist/tools/share-send.d.ts.map +1 -1
- package/dist/tools/share-send.js +32 -7
- package/dist/tools/share-send.js.map +1 -1
- package/dist/tools/signed-split-sheet-preview.d.ts +43 -0
- package/dist/tools/signed-split-sheet-preview.d.ts.map +1 -0
- package/dist/tools/signed-split-sheet-preview.js +67 -0
- package/dist/tools/signed-split-sheet-preview.js.map +1 -0
- package/dist/tools/split-sheets.d.ts +19 -0
- package/dist/tools/split-sheets.d.ts.map +1 -1
- package/dist/tools/split-sheets.js +108 -15
- package/dist/tools/split-sheets.js.map +1 -1
- package/dist/tools/works.d.ts +9 -1
- package/dist/tools/works.d.ts.map +1 -1
- package/dist/tools/works.js +197 -66
- package/dist/tools/works.js.map +1 -1
- package/dist/tools/write-receipt.d.ts +68 -0
- package/dist/tools/write-receipt.d.ts.map +1 -0
- package/dist/tools/write-receipt.js +64 -0
- package/dist/tools/write-receipt.js.map +1 -0
- package/package.json +4 -4
- package/scripts/build-required-schemas.ts +7 -1
- package/server.json +2 -2
package/dist/tools/works.js
CHANGED
|
@@ -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: [
|
|
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",
|
|
@@ -198,8 +210,16 @@ function summariseDossier(dossier) {
|
|
|
198
210
|
return (`Dossier for "${title}": ${parts.join(", ")}. ` +
|
|
199
211
|
`Provenance: ${attestation}. AI disclosure: ${aiDisclosed}. ` +
|
|
200
212
|
`Consent: ${consentState}${consentLevel}. Completeness: ${completeness}. ` +
|
|
201
|
-
`(Full payload under structuredContent.data.dossier; PDF
|
|
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: [
|
|
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); PDF
|
|
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.
|
|
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: "
|
|
641
|
-
"
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
|
|
865
|
-
|
|
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
|
|
873
|
-
|
|
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
|
-
|
|
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
|
-
// -
|
|
1012
|
-
//
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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;
|
|
@@ -1293,7 +1419,8 @@ export class WorksTools {
|
|
|
1293
1419
|
// assembly of everything PICA holds about the song. Opt-in only (in
|
|
1294
1420
|
// OPT_IN_ONLY above), so this branch only runs when the caller lists
|
|
1295
1421
|
// "dossier" explicitly. The whole assembly degrades to null on failure —
|
|
1296
|
-
// never poisons the rest of the inspect. PDF
|
|
1422
|
+
// never poisons the rest of the inspect. The PDF comes from
|
|
1423
|
+
// pica_export_song_dossiers.
|
|
1297
1424
|
if (has("dossier")) {
|
|
1298
1425
|
sectionTasks.push((async () => {
|
|
1299
1426
|
try {
|
|
@@ -1425,13 +1552,33 @@ export class WorksTools {
|
|
|
1425
1552
|
* Update work
|
|
1426
1553
|
*/
|
|
1427
1554
|
async updateWork(args) {
|
|
1428
|
-
const {
|
|
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);
|
|
1429
1563
|
const work = await this.pica.works.update(id, updates);
|
|
1430
1564
|
// Registration integrity floor (Task 9) — same pressure rendering as
|
|
1431
1565
|
// createWork, so an update that clears (or introduces) a gap keeps the
|
|
1432
1566
|
// agent informed without a separate inspect round-trip.
|
|
1433
1567
|
const pressure = await buildIntegrityPressure(this.pica, "work", work.id);
|
|
1434
|
-
|
|
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));
|
|
1435
1582
|
}
|
|
1436
1583
|
/**
|
|
1437
1584
|
* Delete work
|
|
@@ -1441,44 +1588,28 @@ export class WorksTools {
|
|
|
1441
1588
|
return formatDeletion("work", args.id, outcome);
|
|
1442
1589
|
}
|
|
1443
1590
|
/**
|
|
1444
|
-
*
|
|
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.
|
|
1445
1600
|
*/
|
|
1446
1601
|
async verifyWork(args) {
|
|
1447
|
-
const
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
try {
|
|
1451
|
-
const baseUrl = this.pica.works["baseUrl"];
|
|
1452
|
-
const apiKey = this.pica.works["apiKey"];
|
|
1453
|
-
const res = await fetch(`${baseUrl}/admin/export-readiness/${args.id}`, {
|
|
1454
|
-
headers: {
|
|
1455
|
-
Authorization: `Bearer ${apiKey}`,
|
|
1456
|
-
"Content-Type": "application/json",
|
|
1457
|
-
},
|
|
1458
|
-
});
|
|
1459
|
-
if (res.ok) {
|
|
1460
|
-
const json = (await res.json());
|
|
1461
|
-
exportReadiness = json.data ?? json;
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
catch {
|
|
1465
|
-
// Readiness check is best-effort — don't fail verification
|
|
1466
|
-
}
|
|
1467
|
-
const summaryParts = ["Work verified successfully"];
|
|
1468
|
-
if (exportReadiness) {
|
|
1469
|
-
summaryParts.push(`Export readiness: ${exportReadiness.ready ? "ready" : "not ready"}`, `Evidence: ${exportReadiness.evidenceStrength}`, `Attestation: ${exportReadiness.attestationCoverage}%`);
|
|
1470
|
-
if (Array.isArray(exportReadiness.blockers) &&
|
|
1471
|
-
exportReadiness.blockers.length > 0) {
|
|
1472
|
-
summaryParts.push(`Blockers (${exportReadiness.blockers.length}):`);
|
|
1473
|
-
for (const b of exportReadiness.blockers) {
|
|
1474
|
-
summaryParts.push(` ${b.type}: ${b.resolutionHint}`);
|
|
1475
|
-
}
|
|
1476
|
-
}
|
|
1602
|
+
const workId = typeof args.id === "string" ? args.id.trim() : "";
|
|
1603
|
+
if (!workId) {
|
|
1604
|
+
return typedError("VALIDATION_ERROR", "id is required.");
|
|
1477
1605
|
}
|
|
1478
|
-
return
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
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.");
|
|
1482
1613
|
}
|
|
1483
1614
|
/**
|
|
1484
1615
|
* Bulk delete works
|