node-autotask 0.43.0 → 0.45.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 +49 -0
- package/MCP_TOOL_CATALOG.json +156 -1253
- package/MCP_TOOL_MANIFEST.md +154 -220
- package/MCP_TOOL_OVERRIDES.json +8 -62
- package/N8N_MCP_MANIFEST.json +128 -705
- package/N8N_MCP_MANIFEST.md +43 -63
- package/README.md +10 -28
- package/capabilities.json +15315 -22219
- package/dist/{base-CVx2VMSj.d.cts → base-BVoYDlVA.d.cts} +2 -1
- package/dist/{base-CVx2VMSj.d.ts → base-BVoYDlVA.d.ts} +2 -1
- package/dist/capabilities.cjs +2 -2
- package/dist/capabilities.d.cts +2 -1
- package/dist/capabilities.d.ts +2 -1
- package/dist/capabilities.js +1 -1
- package/dist/{chunk-5VHSLJMR.js → chunk-A6LHCJQP.js} +30 -9
- package/dist/{chunk-JY2BWYXY.js → chunk-ASMCTZO2.js} +15314 -22218
- package/dist/{chunk-TH7B5H4P.cjs → chunk-DC3PY4EF.cjs} +1839 -1407
- package/dist/{chunk-4EFXIE53.js → chunk-DXKXQHCX.js} +5 -5
- package/dist/{chunk-SK3GYRNA.cjs → chunk-JO6WI763.cjs} +143 -143
- package/dist/{chunk-Q23JULNN.js → chunk-NSW7JPVS.js} +11529 -13319
- package/dist/{chunk-KTEDPS4X.js → chunk-O4PIPUS4.js} +1583 -1151
- package/dist/{chunk-MJTKK3QG.cjs → chunk-SN4BMQ2N.cjs} +31 -10
- package/dist/{chunk-5BDUMDAQ.cjs → chunk-VXVYFJH3.cjs} +15314 -22218
- package/dist/{chunk-VFLAMMIJ.cjs → chunk-YMBVKVK4.cjs} +10604 -12394
- package/dist/{index-C7rMJ4IV.d.ts → index-C5g1H_hF.d.ts} +1080 -1288
- package/dist/{index-Cm_Xpt7X.d.cts → index-Db0SNnQn.d.cts} +1080 -1288
- package/dist/{index-C3KFMnRT.d.cts → index-c549LvOb.d.cts} +332 -331
- package/dist/{index-C3KFMnRT.d.ts → index-c549LvOb.d.ts} +332 -331
- package/dist/index.cjs +23 -23
- package/dist/index.d.cts +15 -41
- package/dist/index.d.ts +15 -41
- package/dist/index.js +8 -8
- package/dist/operations/index.cjs +5 -2234
- package/dist/operations/index.d.cts +51 -323
- package/dist/operations/index.d.ts +51 -323
- package/dist/operations/index.js +30 -2259
- package/dist/pool.cjs +9 -9
- package/dist/pool.d.cts +4 -3
- package/dist/pool.d.ts +4 -3
- package/dist/pool.js +5 -5
- package/dist/resources/index.cjs +5 -5
- package/dist/resources/index.d.cts +2 -2
- package/dist/resources/index.d.ts +2 -2
- package/dist/resources/index.js +8 -8
- package/dist/types/index.d.cts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/untrusted.cjs +2 -2
- package/dist/untrusted.d.cts +14 -5
- package/dist/untrusted.d.ts +14 -5
- package/dist/untrusted.js +1 -1
- package/docs/API.md +321 -334
- package/docs/LIVE-TESTING.md +4 -6
- package/docs/N8N-ADAPTER.md +26 -26
- package/examples/mcp/schemas.ts +2 -0
- package/examples/mcp/tools-read.ts +3 -2
- package/n8n-mcp.curation.json +3 -3
- package/package.json +10 -9
package/docs/API.md
CHANGED
|
@@ -260,9 +260,9 @@ status → what the vendor does that triggers it.
|
|
|
260
260
|
| `QueryTooLongError` | `QUERY_TOO_LONG` | policy | — | caller-requested `via: 'get'` whose serialized URL would exceed the 2048-char GET cap — the SDK never silently degrades to POST |
|
|
261
261
|
| `StaleObjectError` | `STALE_OBJECT` | conflict | — | the caller-supplied `lastModifiedDate` no longer matches the vendor's current value (no `If-Match`/ETag exists) |
|
|
262
262
|
| `ConfirmationRequiredError` | `CONFIRMATION_REQUIRED` | policy | — | a destructive / approval-gated op invoked without its exact confirmation flag (`invoke` governor, section 7) |
|
|
263
|
-
| `DeleteBoundaryError` | `DELETE_BOUNDARY` | policy | — |
|
|
264
|
-
| `MarkerUnsafeIdError` | `MARKER_UNSAFE_ID` | policy | — | a
|
|
265
|
-
| `RecoveryTruncatedError` | `RECOVERY_TRUNCATED` | policy | — | the
|
|
263
|
+
| `DeleteBoundaryError` | `DELETE_BOUNDARY` | policy | — | a delete on a resource the vendored spec defines no DELETE for — refused PRE-SEARCH (zero wire; the boundary is the spec-derived plan audit, never a raw vendor 405; issue #9). The throwing op (`operations.recovery`) was deleted in #289 — the refusal code stays in the write-outcome taxonomy |
|
|
264
|
+
| `MarkerUnsafeIdError` | `MARKER_UNSAFE_ID` | policy | — | a marker-filtered match whose id is beyond `Number.MAX_SAFE_INTEGER` (unaddressable through the typed delete surface) — refused with the typed code, never a silent skip (issue #9, P1; the throwing op was deleted in #289 — the refusal code stays) |
|
|
265
|
+
| `RecoveryTruncatedError` | `RECOVERY_TRUNCATED` | policy | — | the marker-filtered bounded search tripped its match cap (more matches than `limit`) — refused instead of a silent truncation / silent empty (issue #9; the throwing op was deleted in #289 — the refusal code stays) |
|
|
266
266
|
| `AutotaskError` | `BUDGET_EXCEEDED` | policy | — (the call that would exceed it is not sent) | `operations.revenueRollup`: the pre-walk estimate exceeds `maxVendorCalls` (refused before any stream; `details: { phase: 'estimate', estimate, budget, suggestions }`) or the counting wrapper reached the budget mid-walk (`phase: 'walk'`; the partial fold is discarded) — `suggestedAction` names the narrowing (issue #234) |
|
|
267
267
|
|
|
268
268
|
Retryable by contract: `NETWORK_ERROR`, `TIMEOUT`, `RATE_LIMITED`,
|
|
@@ -287,8 +287,7 @@ retryability: `emptyReason` (the pinned typed empty-reason — the miss's
|
|
|
287
287
|
kind + note + authority + remedy; §11.10) and `pointerNote` (the
|
|
288
288
|
known-unusable-op pointer sentence — the status/code are untouched, the
|
|
289
289
|
note routes to the operation that works), plus the additive refusal /
|
|
290
|
-
drift payloads (the
|
|
291
|
-
shape-drift marker info). A non-pinned surface gets no `details` at all
|
|
290
|
+
drift payloads (the #13 shape-drift `candidateIds` refusal). A non-pinned surface gets no `details` at all
|
|
292
291
|
(no speculation). On the MCP tools' served error payload, `details` is
|
|
293
292
|
passed through verbatim (live-verified 2026-10-04: the W-D3 pointer note
|
|
294
293
|
rides a real `time_entries.getPosted` vendor 500 on the wire; that 500 is gone since 0.32.0, the pointer-note mechanism is unchanged) and the
|
|
@@ -685,6 +684,22 @@ carry paging parameters.
|
|
|
685
684
|
when the limit or the guard truncated the walk and more records exist past
|
|
686
685
|
the stop point; `pageDetails.count` is never surfaced (it is the current
|
|
687
686
|
page's record count — `<resource>.count` is the total).
|
|
687
|
+
- **`walk: { maxRows: N }` — an explicit export cap (issue #350).**
|
|
688
|
+
`N` is a positive integer and replaces the total `limit` cap, so
|
|
689
|
+
`{ walk: { maxRows: 10_000 } }` can return 10,000 rows. `limit` still
|
|
690
|
+
validates as 1..2500 (default 500) and only sizes pages in this mode:
|
|
691
|
+
`min(500, limit)`. `truncated` and `hasMore` are true when the final
|
|
692
|
+
page was sliced, a next cursor remains, or the 1000-page guard stopped
|
|
693
|
+
the walk, even below `maxRows`; an exhausted exact-cap result reports
|
|
694
|
+
false. No extra page is fetched to decide completeness. With no `walk`,
|
|
695
|
+
the ordinary cap and result shape remain unchanged (`truncated` is
|
|
696
|
+
omitted). `walk` and `latest` cannot be combined. Through `invoke`, the
|
|
697
|
+
unscoped-walk guard estimates the explicit `maxRows` cap independently
|
|
698
|
+
of `limit`; narrow the filter or pass `acknowledgeUnscoped: true` when
|
|
699
|
+
a large unscoped walk is intended. When names are enabled (per call or
|
|
700
|
+
by client labels), explicit walks apply the existing label budget per
|
|
701
|
+
fetched vendor page, matching the iterator; invoke does not label the
|
|
702
|
+
combined result again. Ordinary invoked queries keep their per-call budget.
|
|
688
703
|
- **`latest: N` (issue #59) — the N highest-`id` rows without caller paging.**
|
|
689
704
|
The vendor has NO sort parameter (the vendored spec's `QueryModel` is
|
|
690
705
|
exactly `maxRecords` / `includeFields` / `filter`), so `latest` is a
|
|
@@ -780,23 +795,28 @@ Vendor filter structure (swagger `Filter`, recursive):
|
|
|
780
795
|
`project.<field>` is resolved before the query: the parent entity is read
|
|
781
796
|
(`IncludeFields: ['id']`, at most 300 rows) with the relation's conditions,
|
|
782
797
|
and the leaves become `<relation>ID in [ids]`. Supported by `queryBounded`
|
|
783
|
-
(and the invoke `query` op), `count
|
|
784
|
-
`query()`/`queryAll()` walks
|
|
798
|
+
(and the invoke `query` op), `count`, `operations.aggregate`, and the raw
|
|
799
|
+
`query()`/`queryAll()` walks.
|
|
785
800
|
- Inside an `and` (and at the top level) all leaves of one relation share ONE
|
|
786
801
|
parent read; inside an `or` each relation leaf is its own read. Direct
|
|
787
802
|
conditions are untouched and may be mixed freely.
|
|
788
803
|
- No parent match: an `and` matches nothing; an `or` drops that branch. When
|
|
789
804
|
the whole filter matches nothing the child call is not issued: `queryBounded`
|
|
790
805
|
returns `{ items: [], fetched: 0, hasMore: false, pagesWalked: 0,
|
|
791
|
-
relationNote }
|
|
806
|
+
relationNote }` (plus `truncated: false` for an explicit `walk`),
|
|
807
|
+
`count` returns `0`, aggregate group/sum carries
|
|
792
808
|
`relationNote`.
|
|
793
809
|
- More than 300 matching parents: typed `AutotaskConfigError`, child call not
|
|
794
810
|
issued (a capped parent set would be vendor-ordered, so partial totals would
|
|
795
811
|
be biased). Narrow the relation conditions (e.g. a ticket date or status).
|
|
796
812
|
- Pre-request (zero wire): unknown relation, a parent field outside the
|
|
797
|
-
parent's vendored field set
|
|
798
|
-
|
|
813
|
+
parent's vendored field set and multi-hop (`ticket.company.x`) are
|
|
814
|
+
refused. The invoke field-name check skips relation
|
|
799
815
|
leaves (they are checked against the parent instead).
|
|
816
|
+
- Any dotted `udf: true` field is refused before metadata, parent queries,
|
|
817
|
+
scope counts or child requests, including self prefixes and unknown
|
|
818
|
+
prefixes (issue #351). Use an undotted tenant UDF name with `udf: true`;
|
|
819
|
+
`fieldCheck: false` does not bypass this rule.
|
|
800
820
|
|
|
801
821
|
```ts
|
|
802
822
|
// company 123's time in October, hours per ticket: 2 calls (tickets ids, then the walk)
|
|
@@ -829,11 +849,20 @@ Vendor filter structure (swagger `Filter`, recursive):
|
|
|
829
849
|
the cap). A GET path exists (`{ via: 'get' }`) and the SDK measures the
|
|
830
850
|
serialized GET URL — over 2048 it throws `QueryTooLongError` (never a
|
|
831
851
|
silent degradation).
|
|
832
|
-
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
852
|
+
- **Own-field aliases (issue #352):** a single self prefix — the resource
|
|
853
|
+
key, or a relation name targeting that resource — is stripped from
|
|
854
|
+
ordinary filter fields, `includeFields`, and aggregate `groupBy` / `sum`.
|
|
855
|
+
On tickets, `ticket.title` and `tickets.title` mean `title`; on time
|
|
856
|
+
entries, `time_entries.hoursWorked` means `hoursWorked`, while
|
|
857
|
+
`ticket.companyID` remains a parent relation filter. Aggregate date
|
|
858
|
+
suffixes are preserved (`time_entries.dateWorked@month` becomes
|
|
859
|
+
`dateWorked@month`), and result keys use the canonical unprefixed names.
|
|
860
|
+
Foreign and multi-hop names are never converted into own-field aliases.
|
|
861
|
+
Projection normalization applies on the first page and every cursor
|
|
862
|
+
continuation, without mutating the caller's input.
|
|
863
|
+
- `includeFields` selects vendor columns after own-field normalization; the
|
|
864
|
+
SDK always appends `id` when omitted (the vendor omits `id` unless it is
|
|
865
|
+
selected, and callers need it to act on the row — issue #59).
|
|
837
866
|
|
|
838
867
|
## 6. Mutation safety
|
|
839
868
|
|
|
@@ -962,7 +991,48 @@ candidates and the error code when a resource could not resolve (empty
|
|
|
962
991
|
the primary text field, exact match only; with `resolution.scanOnMiss` the
|
|
963
992
|
bounded client-scan re-check (and its truncation cap) applies per resource. Returns
|
|
964
993
|
`{ identifier, results: [{ resource, value, resolutionCost, scanned,
|
|
965
|
-
scanTruncated, candidates, error? }], matches }`.
|
|
994
|
+
scanTruncated, candidates, error? }], matches, matched }`.
|
|
995
|
+
|
|
996
|
+
`match` defaults to `'exact'`. With `match: 'auto'`, every requested
|
|
997
|
+
resource finishes its exact lookup before a contains search can start. Only
|
|
998
|
+
a clean miss everywhere permits the contains sweep; an exact hit,
|
|
999
|
+
ambiguity, truncated scan, unsupported resolver or lookup error suppresses
|
|
1000
|
+
it. Numeric ids and digit strings never fall back to contains. Auth,
|
|
1001
|
+
rate-limit, server, network, timeout and policy faults propagate.
|
|
1002
|
+
|
|
1003
|
+
The contains sweep defaults to the client’s `resolution.maxScanRecords`,
|
|
1004
|
+
capped at 500. Optional `limit` must be an integer 1..500 and can only
|
|
1005
|
+
narrow that configured cap; invalid values refuse before lookup wire.
|
|
1006
|
+
Exact lookup sizes are unchanged. A singleton below the cap resolves; multiple matches
|
|
1007
|
+
report candidate ids, and a cap-full result reports `scanTruncated: true`
|
|
1008
|
+
and `RESOLUTION_TRUNCATED`, even if the cap is one. `matched` reports
|
|
1009
|
+
`'exact'` or `'contains'`; only the latter includes the full sweep in
|
|
1010
|
+
`contains`, including an empty sweep.
|
|
1011
|
+
|
|
1012
|
+
**`resolveStaff(client, identifier, opts?)`** looks up Autotask resources
|
|
1013
|
+
by exact email, userName or name. Text matches require `isActive: true`
|
|
1014
|
+
and exclude API users by default, using tenant picklist labels on
|
|
1015
|
+
`licenseType` / `userType` rather than fixed numeric values. Numeric ids
|
|
1016
|
+
fetch directly and bypass active/API-user exclusions. `emailOnly: true`
|
|
1017
|
+
treats even digit strings as email; `includeApiUsers: true` includes API
|
|
1018
|
+
users in text matches. Options form a closed plain-object bag: flags must be
|
|
1019
|
+
booleans when present and limit must be an integer 1..10. Unknown keys, nulls
|
|
1020
|
+
and malformed options refuse with CONFIG_ERROR before metadata or lookup wire.
|
|
1021
|
+
|
|
1022
|
+
Returns `{ identifier, lookup: 'id' | 'text', matches: [{ record, apiUser?,
|
|
1023
|
+
licenseTypeLabel? }], matchedCount, truncated, metadataState }`. `limit`
|
|
1024
|
+
is 1..10 (default 10); the text lookup reads at most 50 records.
|
|
1025
|
+
`matchedCount` is after exclusions and before the candidate limit, and is
|
|
1026
|
+
a lower bound when truncated. A live cursor or a candidate-limit cut sets
|
|
1027
|
+
`truncated: true`; a truncated singleton never proves uniqueness. Text
|
|
1028
|
+
misses return an empty `matches`; a numeric miss propagates `NOT_FOUND`.
|
|
1029
|
+
`metadataState` is `ok` when API-user labels were found, `none` when labels
|
|
1030
|
+
exist without an API label, `absent` when no labels are exposed, or `error`
|
|
1031
|
+
when metadata could not be read. Lookup continues if metadata is
|
|
1032
|
+
unavailable, and API-user classification is omitted when unproven. Text
|
|
1033
|
+
records honor the existing client `labels` setting through the shared row-label
|
|
1034
|
+
pass; optional `names` reports incomplete label coverage. Numeric gets keep
|
|
1035
|
+
their ordinary record shape.
|
|
966
1036
|
|
|
967
1037
|
### 7.3 `invoke(client, operation, args?, opts?)` — the write governor
|
|
968
1038
|
|
|
@@ -989,7 +1059,8 @@ the mode pinned (§7.4). The governor rules (pinned):
|
|
|
989
1059
|
`queryBounded`** (server-side pagination): ONE invoke call returns a
|
|
990
1060
|
bounded, complete-enough result — the vendor cursor walk happens inside
|
|
991
1061
|
the SDK; the caller passes `limit` as the TOTAL cap (default 500, max
|
|
992
|
-
2500)
|
|
1062
|
+
2500), or an independent `walk.maxRows` export cap (section 4), and never
|
|
1063
|
+
owns a stream. `queryAll` keeps its raw behavior (the
|
|
993
1064
|
unbounded walk; the catalog text steers the caller away for normal work);
|
|
994
1065
|
- a query's `includeFields` NAMES and its `filter` condition FIELD NAMES are
|
|
995
1066
|
validated **pre-request** (issues #119b + #125): the authority is the
|
|
@@ -1059,118 +1130,45 @@ be cross-mode-refused.
|
|
|
1059
1130
|
Pinned by the full-surface suite (`test/operations/invoke/modes.test.ts`:
|
|
1060
1131
|
all 1281 ops × 3 modes, zero wire).
|
|
1061
1132
|
|
|
1062
|
-
|
|
1063
|
-
whose MCP annotation is `readOnlyHint !== true` — today exactly **
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
`autotask_catalog` + `autotask_describe`) all carry `readOnlyHint: true`
|
|
1068
|
-
and are safe ungated: every op they can route is `effect=read` (zero
|
|
1069
|
-
mutating requests — pinned by the test inventory, §6 of the split design).
|
|
1070
|
-
Refined class for the highest-risk ops: the **57 tools with
|
|
1071
|
-
`destructiveHint: true`** (56 dedicated delete tools + `autotask_delete`)
|
|
1072
|
-
additionally require `confirm === <operation key>` even inside
|
|
1073
|
-
`autotask_delete`, and the 3 approval-gated writes
|
|
1074
|
-
(`configuration_items.moveConfigurationItem`, `contacts.moveToCompany`,
|
|
1075
|
-
`resources.transferOwnership`) require the same confirm inside
|
|
1076
|
-
`autotask_write`. Migration: `autotask_invoke` is REPLACED by
|
|
1077
|
-
`autotask_read`/`autotask_write`/`autotask_delete` — update harness config
|
|
1078
|
-
(approve the 267 above) and remove any `autotask_invoke` entry. The rule is
|
|
1079
|
-
derivable from the registry `effect` column (and the mode tools' fixed
|
|
1080
|
-
annotations) — it never needs a hand-maintained name list.
|
|
1081
|
-
|
|
1082
|
-
### 7.5 `recovery(client, input, opts?)` — the one-call recovery/cleanup op (issue #9)
|
|
1133
|
+
<!-- BEGIN GENERATED:approval-policy -->
|
|
1134
|
+
**Harness approval policy (recommended).** Require approval on every tool whose MCP annotation is `readOnlyHint !== true` — today exactly **246 tools**: the 244 dedicated write/delete tools (188 `effect=write` + 56 `effect=destructive`, every one of them has a dedicated tool) + `autotask_write` + `autotask_delete`. The remaining **599 tools** (the 586 dedicated read tools + the 10 core read tools + `autotask_read` + `autotask_catalog` + `autotask_describe`) all carry `readOnlyHint: true` and are safe ungated: every op they can route is `effect=read` (zero mutating requests). Refined class for the highest-risk ops: the **57 tools with `destructiveHint: true`** (56 dedicated delete tools + `autotask_delete`) additionally require `confirm === <operation key>` even inside `autotask_delete`. The approval-gated writes (`configuration_items.moveConfigurationItem`, `contacts.moveToCompany`) require the same confirm inside `autotask_write`. Migration: `autotask_invoke` is REPLACED by `autotask_read`/`autotask_write`/`autotask_delete` — update harness config and remove any `autotask_invoke` entry. The rule is derivable from the registry `effect` column and the mode tools’ fixed annotations; it never needs a hand-maintained name list.
|
|
1135
|
+
<!-- END GENERATED:approval-policy -->
|
|
1136
|
+
|
|
1137
|
+
### 7.5 the `create` dry-run delete-boundary warning (issue #9)
|
|
1083
1138
|
|
|
1084
1139
|
The delete-boundary reality (§11.1, §11.9): the vendored spec defines NO
|
|
1085
1140
|
DELETE on many entity paths — most of them the campaign's own resources. A
|
|
1086
1141
|
`create` that cannot be undone through the API leaves the created record as
|
|
1087
|
-
permanent residue once its id is lost (P1).
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
```ts
|
|
1093
|
-
import { recovery } from 'node-autotask/operations';
|
|
1094
|
-
// find the scratch record a write just stamped (ONE bounded query, zero mutating wire):
|
|
1095
|
-
const r = await recovery(client, { resource: 'companies', marker: 'FULL100W-1791110339' });
|
|
1096
|
-
// r.delete.available === false + r.cleanup.note — the spec says the vendor has no DELETE.
|
|
1097
|
-
// where the vendor DOES allow it (delete available):
|
|
1098
|
-
const dry = await recovery(client, { resource: 'configuration_item_dns_records', marker: 'm', field: 'data', parentId: 42, action: 'delete' });
|
|
1099
|
-
const applied = await recovery(client, { resource: 'configuration_item_dns_records', marker: 'm', field: 'data', parentId: 42, action: 'delete' },
|
|
1100
|
-
{ dryRun: false, confirm: 'operations.recovery' });
|
|
1101
|
-
```
|
|
1142
|
+
permanent residue once its id is lost (P1). The one-call recovery op that
|
|
1143
|
+
turned the boundary into a typed, bounded path (the marker-filtered find +
|
|
1144
|
+
confirm-gated delete that shared the SAME generated boundary note) was
|
|
1145
|
+
deleted in #289 — no server consumer; the boundary data, the typed error
|
|
1146
|
+
codes and the dry-run warning remain (§11.9).
|
|
1102
1147
|
|
|
1103
|
-
-
|
|
1104
|
-
(server-side pagination; default `limit` 25, max 100) returns the matched
|
|
1105
|
-
records' ids (`matches: [{ id, value }]`) + the delete-boundary fact
|
|
1106
|
-
(`delete: { available, reason, authority }`) + the UI-only `cleanup` note.
|
|
1107
|
-
Zero mutating wire.
|
|
1108
|
-
- **`delete`** (destructive, dry-run-first): the dry run performs the search
|
|
1109
|
-
and reports the match set with **zero DELETE wire** (`simulated: true`);
|
|
1110
|
-
the apply requires the exact confirm string (`confirm ===
|
|
1111
|
-
'operations.recovery'` — the governor forwards the caller's `confirm`
|
|
1112
|
-
through the trailing bag) and issues one DELETE per safe match id + one
|
|
1113
|
-
verification read per id (`deleted: [{ id, verified, code? }]`).
|
|
1114
|
-
- **the marker field**: the `contains` filter runs on the name-like field
|
|
1115
|
-
the write stamped — default the plan's `markerField` for the resource
|
|
1116
|
-
(the spec-derived deterministic priority over the vendored model's field
|
|
1117
|
-
set), pass `field` to override; REQUIRED when the resource has no
|
|
1118
|
-
`markerField` (a `CONFIG_ERROR` names the candidates).
|
|
1119
|
-
- **P1 discipline — never a silent failure mode**:
|
|
1120
|
-
- a delete on a delete-less resource refuses **PRE-SEARCH** with the typed
|
|
1121
|
-
`DELETE_BOUNDARY` (zero wire — the boundary is the local spec audit,
|
|
1122
|
-
never a raw vendor 405);
|
|
1123
|
-
- a match whose id is beyond `Number.MAX_SAFE_INTEGER` is never a silent
|
|
1124
|
-
skip: `delete` refuses with the typed `MARKER_UNSAFE_ID` (zero DELETE
|
|
1125
|
-
wire); `find` marks the match `id: null` + `unsafeId: true`;
|
|
1126
|
-
- a cap-tripped search (more matches than `limit`) refuses with the typed
|
|
1127
|
-
`RECOVERY_TRUNCATED` — never a silent truncation, never a silent empty;
|
|
1128
|
-
- an unknown `resource` refuses with the typed `RESOURCE_UNKNOWN` (the
|
|
1129
|
-
same code `operations.relationshipMap` uses: carries the resource count
|
|
1130
|
-
+ the `autotask_catalog` pointer).
|
|
1131
|
-
- **the create dry run warns**: on a delete-less resource,
|
|
1148
|
+
- **The create dry run warns**: on a delete-less resource,
|
|
1132
1149
|
`client.<resource>.create(data, { dryRun: true })` carries the
|
|
1133
1150
|
report-only `delete-boundary` check (`ok: true` — `wouldApply` is
|
|
1134
1151
|
preserved) + the boundary note in `warnings[]` (the SAME generated note
|
|
1135
|
-
|
|
1136
|
-
Pinned by `test/
|
|
1137
|
-
governor suite (`test/operations/invoke/modes.test.ts`).
|
|
1152
|
+
the describe channel carries — the describe and dry-run channels cannot
|
|
1153
|
+
drift). Pinned by `test/resources/mutation-safety.test.ts` + the
|
|
1154
|
+
full-surface governor suite (`test/operations/invoke/modes.test.ts`).
|
|
1138
1155
|
|
|
1139
|
-
### 7.6 `
|
|
1156
|
+
### 7.6 `fetchByIds(client, items, opts?)` — bulk fetch by ids
|
|
1140
1157
|
|
|
1141
|
-
|
|
1158
|
+
The cross-resource bulk read with per-item typed results — one entry per
|
|
1142
1159
|
input occurrence: `{ resource, id, found, value, error?: { code, httpStatus?,
|
|
1143
1160
|
message } }`; a NOT_FOUND or fault on one item never aborts the rest; the
|
|
1144
|
-
result is `{ results, ok, failed }`.
|
|
1161
|
+
result is `{ results, ok, failed }`. The op refuses pre-request (zero wire):
|
|
1145
1162
|
non-numeric string ids (pointing at `operations.resolveAny` — canonical
|
|
1146
1163
|
integer strings above 2^53 pass with their exact digits), parent-scoped
|
|
1147
1164
|
children (their `get` needs `parentId`), unknown `fields` keys (the complete
|
|
1148
1165
|
problem list in one message), unsafe number ids (the canonical-string
|
|
1149
1166
|
pointer). The op never fabricates rows: values come only from a vendor 2xx
|
|
1150
1167
|
(query row or GET record); a miss is a typed outcome, never a null-padded
|
|
1151
|
-
row.
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
one sequential GET per item. Per-item `fields?: string[]` is a client-side
|
|
1156
|
-
projection (the GET returns the full model; projection is post-fetch, zero
|
|
1157
|
-
extra wire).
|
|
1158
|
-
**Live field validation (opt-in `opts.liveFields`, issue #66)**: when
|
|
1159
|
-
`true`, each item's `fields` validate against the tenant's **live** entity
|
|
1160
|
-
field metadata (the existing cached metadata channel — with
|
|
1161
|
-
`fieldCache` configured a warm cache costs zero wire; a cold run is one
|
|
1162
|
-
`entityInformation/fields` GET per DISTINCT resource in the batch) instead
|
|
1163
|
-
of the vendored-spec field set alone. A resource whose metadata is
|
|
1164
|
-
unavailable (network fault, vendor 5xx) falls back to the vendored-spec
|
|
1165
|
-
set for THAT resource only — unavailability never throws and never blocks
|
|
1166
|
-
the batch. When available the live set is authoritative: a field it lacks
|
|
1167
|
-
is refused even if the spec lists it, and a live field the spec lacks is
|
|
1168
|
-
accepted — a tenant whose live TimeEntry entity exposes `postedDate` (the
|
|
1169
|
-
live record carries it even when the vendored model omits it) may project
|
|
1170
|
-
it once `liveFields: true`; the default spec-only path still refuses it.
|
|
1171
|
-
The default (no `liveFields`) is today's vendored-spec-only validation,
|
|
1172
|
-
bit-for-bit, zero metadata wire. Field discovery in both modes:
|
|
1173
|
-
`operations.fieldCatalog`.
|
|
1168
|
+
row. The per-item result shapes and the pre-request validation taxonomy are
|
|
1169
|
+
reused from the deleted `fetchMany` op (issue #289 — the shared `FetchMany*`
|
|
1170
|
+
result types kept their names).
|
|
1171
|
+
|
|
1174
1172
|
- **`fetchByIds`** — up to 500 items (mixed resources; the pinned
|
|
1175
1173
|
`FETCH_BY_IDS_MAX_ITEMS`; `opts.maxItems` may lower the cap, never raise
|
|
1176
1174
|
it; over → `CONFIG_ERROR` pre-request, zero wire). **Query-first**: per
|
|
@@ -1185,8 +1183,7 @@ row.
|
|
|
1185
1183
|
limited to the plan-pinned blind surfaces). Wire cost: per resource
|
|
1186
1184
|
`ceil(uniqueIds/250)` query calls + one `get` per query-missed id — a
|
|
1187
1185
|
clean 500-id single-resource batch is **2 wire** (down from 500 sequential
|
|
1188
|
-
GETs); the blind-surface worst case is 2 + up to 500 gets
|
|
1189
|
-
parity, never worse). A chunk-level vendor fault faults every item of that
|
|
1186
|
+
GETs); the blind-surface worst case is 2 + up to 500 gets. A chunk-level vendor fault faults every item of that
|
|
1190
1187
|
chunk (the chunk's `code`/`httpStatus`) and the remaining chunks still run
|
|
1191
1188
|
(input order is preserved across chunks and groups; a duplicate id gets its
|
|
1192
1189
|
own entry with the identical outcome over one wire fetch).
|
|
@@ -1282,10 +1279,12 @@ wire):
|
|
|
1282
1279
|
vendor casing via `operations.fieldCatalog`). An empty array / absent = the
|
|
1283
1280
|
unfiltered call (the vendor-accepted no-filter shape rides the wire as
|
|
1284
1281
|
`filter: []`; that is also the blind-predicate's stamp case).
|
|
1285
|
-
- **`groupBy` / `sum`** =
|
|
1282
|
+
- **`groupBy` / `sum`** = vendor field names, accepting the single own-field
|
|
1283
|
+
prefix aliases of section 5 (normalized before validation and folding;
|
|
1284
|
+
result keys are unprefixed). Names are validated against the
|
|
1286
1285
|
resource's vendored field set pre-request — a hallucinated field is a shape
|
|
1287
1286
|
error, not a silent fold-miss; the complete problem list in one typed error,
|
|
1288
|
-
zero wire
|
|
1287
|
+
zero wire. Non-numeric/absent `sum` values contribute 0 (documented).
|
|
1289
1288
|
- **`top`** (default 25, clamped 1..100) keeps the top-N groups (descending
|
|
1290
1289
|
count; the deterministic group-key order breaks ties). **`limit`** (default
|
|
1291
1290
|
2500 = the house `BOUNDED_QUERY_MAX_LIMIT`, clamped down to it) is the
|
|
@@ -1411,14 +1410,17 @@ notes cannot be deleted, so they never use create-then-delete.
|
|
|
1411
1410
|
|
|
1412
1411
|
For per-ticket status / assignee / company use `operations.fetchByIds` on the group ticket ids
|
|
1413
1412
|
(with `names: true`: `statusLabel`, `assignedResourceIDName`, `companyIDName`).
|
|
1414
|
-
- **`
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
behind UTC, e.g. `America/Los_Angeles`, selected the NEXT UTC day; live-verified 2026-10-23.)
|
|
1413
|
+
- **`dateWorked` windows are whole UTC days (issue #66; consolidated in #290)**: the vendor stores
|
|
1414
|
+
`dateWorked` as a date-only value at 00:00 UTC, so the wire bounds are always whole UTC days.
|
|
1415
|
+
`time_entries.dateWorkedWindow` takes `from`/`to` as `YYYY-MM-DD` and the days ARE the bounds — the
|
|
1416
|
+
`tz` option is gone with the consolidation (before it, `tz` was validated but never shifted these
|
|
1417
|
+
bounds), and the relative windows (`today`, `last7`, `lastFullMonth`, ...) are cut. (Before 0.24.0 a
|
|
1418
|
+
`tz` behind UTC, e.g. `America/Los_Angeles`, selected the NEXT UTC day; live-verified 2026-10-23.)
|
|
1419
|
+
|
|
1420
|
+
### 7.12 `timeEntries.approve` — time-entry approval levels (2026-10-26; cut in #292)
|
|
1420
1421
|
|
|
1421
|
-
|
|
1422
|
+
**Cut in #292** (confirmed unused write op — the CHANGELOG names the raw-composition
|
|
1423
|
+
alternative). The vendor facts below stand for anyone composing it by hand:
|
|
1422
1424
|
|
|
1423
1425
|
`timeEntries.approve(id, opts?)` records one approval level for a time entry.
|
|
1424
1426
|
The vendor keeps approvals in a separate entity (`BillingItemApprovalLevels`)
|
|
@@ -1550,9 +1552,8 @@ over a window, by account vertical": the registry op `operations.revenueRollup`
|
|
|
1550
1552
|
label }`, absent otherwise). A label shared by several live picklist ids
|
|
1551
1553
|
selects every one of them.
|
|
1552
1554
|
- **`groupBy`** (default `['vertical', 'contractType']`): `vertical`
|
|
1553
|
-
(`companies.marketSegmentID`), `contractType`, `
|
|
1554
|
-
|
|
1555
|
-
dimensions (#235):
|
|
1555
|
+
(`companies.marketSegmentID`), `contractType`, `company`, `month` (the
|
|
1556
|
+
invoice month), and the join dimensions (#235):
|
|
1556
1557
|
- `contract` — `{ id, name }` (`contractName` joins the contract fetch).
|
|
1557
1558
|
- `lineOfBusiness` — per billing line (`organizationalLevelAssociationID`;
|
|
1558
1559
|
no contract-pointer fallback): three batch `id in` fetches (associations,
|
|
@@ -1563,26 +1564,18 @@ over a window, by account vertical": the registry op `operations.revenueRollup`
|
|
|
1563
1564
|
their amount (LOB figures are floors). A row's `contracts` is "contracts
|
|
1564
1565
|
with at least one line in this LOB", so per-LOB counts can sum to more than
|
|
1565
1566
|
`totals.contracts`.
|
|
1566
|
-
- `resource` — labour lines only (`billingItemType` Labor / Labor
|
|
1567
|
-
Adjustment, or the FK fallback): `timeEntryID` → time entry → resource,
|
|
1568
|
-
`{ id: resourceID, name: 'First Last' }`. Non-labour lines, and labour
|
|
1569
|
-
lines with no time entry (e.g. an adjustment), are `{ id: null, label: 'n/a' }`.
|
|
1570
|
-
- `billingCode` — `billingCodeID` → billing code `{ id, name }`; a line
|
|
1571
|
-
without one is `{ id: null, label: 'none' }`.
|
|
1572
1567
|
|
|
1573
1568
|
Each row key is `{ id, label?, name?, division? }`: picklist text in
|
|
1574
|
-
`label`; vendor entity text (untrusted — company, contract
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
its
|
|
1583
|
-
|
|
1584
|
-
the line projection grows by `organizationalLevelAssociationID`,
|
|
1585
|
-
`timeEntryID` / the type fields and `billingCodeID` the same way.
|
|
1569
|
+
`label`; vendor entity text (untrusted — company, contract and LOB level2
|
|
1570
|
+
names) in `name`, and the LOB division (level1 name, also untrusted) in
|
|
1571
|
+
`division`. Missing values get their own row (`unassigned`,
|
|
1572
|
+
`no LOB assigned`); a value the picklist does not serve stays id-only and is
|
|
1573
|
+
listed in `coverage.blindSurfaces`. A join id the `id in` fetch does not
|
|
1574
|
+
return is never guessed: the line goes to
|
|
1575
|
+
`coverage.excluded.unresolvedAssociations` (association or its level1/level2)
|
|
1576
|
+
with its amount. The bucket (and `coverage.lineOfBusiness`) appears only
|
|
1577
|
+
when its dimension is grouped; the line projection grows by
|
|
1578
|
+
`organizationalLevelAssociationID` the same way.
|
|
1586
1579
|
- **Rows** are sorted by revenue descending, then ids; `top` (default 100,
|
|
1587
1580
|
max 100) keeps the head and folds the tail into `rowsTruncated`. `totals`
|
|
1588
1581
|
always cover every row.
|
|
@@ -1709,19 +1702,19 @@ const lob = await revenueRollup(client, { tenantWide: true, dateFrom: '2025-07-0
|
|
|
1709
1702
|
## 8. Resource surface
|
|
1710
1703
|
|
|
1711
1704
|
<!-- BEGIN GENERATED:api-per-entity -->
|
|
1712
|
-
Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page guarded), `queryBounded` (server-side pagination — one call walks the cursor until `limit`
|
|
1705
|
+
Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page guarded), `queryBounded` (server-side pagination — one call walks the cursor until the total cap: `limit` (default 500 / max 2500), or independent `walk.maxRows` for exports (pages stay `min(500, limit)`); `hasMore` reports incomplete walks, and explicit walks also carry `truncated`; the MCP `query` surface, routed by `invoke`), `queryAll` (collect → `T[]` — the unbounded raw walk), `count`, `get` (id → record; `item: null` → `NotFoundError`), `create` / `update` / `delete` (where the vendor defines them — mutations take a `MutationOpts` bag: `{ dryRun, lastModifiedDate?, maxRetries? }` (issue #105: `maxRetries` overrides the configured retry budget for every request the mutation issues; `0` = exactly one send per request)), plus the helper floor where the plan declares it: `resolve` (122 of 129 — 7 lack it), `search` (41 — bounded contains on the resource text field), `getContext` (5 workflow resources); the `findBy<Field>` methods (findBySerial, findByTicketNumber, findByEmail) are class methods, not plan-declared special ops. Entity metadata on every resource: `getEntityInfo`, `getFieldInfo`, `getUdfInfo` (UDF-capable 16). Parent-scoped children take `(parentId, id)` on the CRUD methods and require `parentId` on `createIfNotExists` (the scoped create POSTs the scoped collection; the dedup query stays global); `query`/`count`/`resolve`/`search` stay global (narrow with a `parentField` filter). Scoped `update` PATCHes the collection (the vendor keys the record by the id in the body).
|
|
1713
1706
|
|
|
1714
1707
|
| resource key | apiEntity | kind | tier | parent | standard ops present | special ops | mcpTool |
|
|
1715
1708
|
|--------------|-----------|------|------|--------|----------------------|-------------|---------|
|
|
1716
1709
|
| `appointments` | Appointment | entity | extended | no | query, count, get, create, update, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_appointment_get |
|
|
1717
1710
|
| `billing_codes` | BillingCode | entity | extended | no | query, count, get, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_billingCode_get |
|
|
1718
1711
|
| `billing_item_approval_levels` | BillingItemApprovalLevel | entity | extended | no | query, count, get, create | — | autotask_billingItemApprovalLevel_get |
|
|
1719
|
-
| `billing_items` | BillingItems | entity | extended | no | query, count, get, update, resolve |
|
|
1720
|
-
| `change_request_links` | ChangeRequestLink | entity | extended | no | query, count, get, create, delete, resolve |
|
|
1712
|
+
| `billing_items` | BillingItems | entity | extended | no | query, count, get, update, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_billingItems_get |
|
|
1713
|
+
| `change_request_links` | ChangeRequestLink | entity | extended | no | query, count, get, create, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_changeRequestLinks_get |
|
|
1721
1714
|
| `checklist_libraries` | ChecklistLibrary | entity | extended | no | query, count, get, create, update, delete, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_checklistLibrary_get |
|
|
1722
1715
|
| `checklist_library_checklist_items` | ChecklistLibraryChecklistItem | entity | extended | yes (checklist_libraries) | query, count, get, create, update, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_checklistLibraryChecklistItem_get |
|
|
1723
1716
|
| `classification_icons` | ClassificationIcon | entity | advanced | no | query, count, get, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_classificationIcon_get |
|
|
1724
|
-
| `companies` | Company | entity | core | no | query, count, get, create, update, resolve, search, getContext | fullProfile, getEntityInfo, getFieldInfo, getUdfInfo, prepareCrmWrite, searchByDomain, searchByIdentity, unbilledWork
|
|
1717
|
+
| `companies` | Company | entity | core | no | query, count, get, create, update, resolve, search, getContext | fullProfile, getEntityInfo, getFieldInfo, getUdfInfo, prepareCrmWrite, searchByDomain, searchByIdentity, unbilledWork | autotask_company_get |
|
|
1725
1718
|
| `company_alerts` | CompanyAlert | entity | extended | yes (companies) | query, count, get, create, update, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_companyAlert_get |
|
|
1726
1719
|
| `company_locations` | CompanyLocation | entity | extended | yes (companies) | query, count, get, create, update, delete, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_companyLocation_get |
|
|
1727
1720
|
| `company_notes` | CompanyNote | entity | extended | yes (companies) | query, count, get, create, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_companyNote_get |
|
|
@@ -1736,15 +1729,15 @@ Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page gu
|
|
|
1736
1729
|
| `configuration_item_ssl_subject_alternative_names` | ConfigurationItemSslSubjectAlternativeName | entity | extended | yes (configuration_items) | query, count, get, update, delete, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_configurationItemSslSubjectAlternativeName_get |
|
|
1737
1730
|
| `configuration_item_types` | ConfigurationItemType | entity | extended | no | query, count, get, create, update, delete, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_configurationItemTypes_get |
|
|
1738
1731
|
| `configuration_item_webhooks` | ConfigurationItemWebhook | entity | extended | yes (configuration_items) | query, count, get, create, update, delete, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_configurationItemWebhook_get |
|
|
1739
|
-
| `configuration_items` | ConfigurationItem | entity | core | no | query, count, get, create, update, resolve, search, getContext |
|
|
1732
|
+
| `configuration_items` | ConfigurationItem | entity | core | no | query, count, get, create, update, resolve, search, getContext | findBySerial, getEntityInfo, getFieldInfo, getUdfInfo, moveConfigurationItem, profile | autotask_configurationItems_get |
|
|
1740
1733
|
| `contact_group_contacts` | ContactGroupContacts | entity | extended | no | query, count, get, create, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_contactGroupContacts_get |
|
|
1741
1734
|
| `contact_groups` | ContactGroups | entity | extended | no | query, count, get, create, update, delete, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_contactGroups_get |
|
|
1742
1735
|
| `contact_webhooks` | ContactWebhook | entity | extended | yes (contacts) | query, count, get, create, update, delete, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_contactWebhook_get |
|
|
1743
|
-
| `contacts` | Contact | entity | core | yes (companies) | query, count, get, create, update, delete, resolve, search, getContext | findByEmail, getEntityInfo, getFieldInfo, getUdfInfo, moveToCompany
|
|
1736
|
+
| `contacts` | Contact | entity | core | yes (companies) | query, count, get, create, update, delete, resolve, search, getContext | findByEmail, fullProfile, getEntityInfo, getFieldInfo, getUdfInfo, moveToCompany | autotask_contact_get |
|
|
1744
1737
|
| `contract_billing_rules` | ContractBillingRule | entity | extended | yes (contracts) | query, count, get, create, update, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_contractBillingRule_get |
|
|
1745
1738
|
| `contract_block_hour_factors` | ContractBlockHourFactor | entity | extended | yes (contracts) | query, count, get, create, update, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_contractBlockHourFactor_get |
|
|
1746
1739
|
| `contract_blocks` | ContractBlock | entity | extended | yes (contracts) | query, count, get, create, update, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_contractBlock_get |
|
|
1747
|
-
| `contract_charges` | ContractCharge | entity | extended | yes (contracts) | query, count, get, create, update, delete, resolve, search |
|
|
1740
|
+
| `contract_charges` | ContractCharge | entity | extended | yes (contracts) | query, count, get, create, update, delete, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_contractCharge_get |
|
|
1748
1741
|
| `contract_exclusion_billing_codes` | ContractExclusionBillingCode | entity | extended | yes (contracts) | query, count, get, create, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_contractExclusionBillingCode_get |
|
|
1749
1742
|
| `contract_exclusion_roles` | ContractExclusionRoles | entity | extended | yes (contracts) | query, count, get, create, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_contractExclusionRoles_get |
|
|
1750
1743
|
| `contract_exclusion_set_excluded_roles` | ContractExclusionSetExcludedRoles | entity | extended | yes (contract_exclusion_sets) | query, count, get, create, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_contractExclusionSetExcludedRole_get |
|
|
@@ -1760,22 +1753,22 @@ Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page gu
|
|
|
1760
1753
|
| `contract_service_bundle_units` | ContractServiceBundleUnit | entity | extended | yes (contracts) | query, count, get, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_contractServiceBundleUnit_get |
|
|
1761
1754
|
| `contract_service_bundles` | ContractServiceBundle | entity | extended | yes (contracts) | query, count, get, create, update, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_contractServiceBundle_get |
|
|
1762
1755
|
| `contract_service_units` | ContractServiceUnit | entity | extended | yes (contracts) | query, count, get, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_contractServiceUnit_get |
|
|
1763
|
-
| `contract_services` | ContractService | entity | extended | yes (contracts) | query, count, get, create, update, resolve |
|
|
1756
|
+
| `contract_services` | ContractService | entity | extended | yes (contracts) | query, count, get, create, update, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_contractService_get |
|
|
1764
1757
|
| `contract_ticket_purchases` | ContractTicketPurchases | entity | extended | yes (contracts) | query, count, get, create, update, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_contractTicketPurchase_get |
|
|
1765
|
-
| `contracts` | Contract | entity | extended | no | query, count, get, create, update, resolve, search |
|
|
1758
|
+
| `contracts` | Contract | entity | extended | no | query, count, get, create, update, resolve, search | fullProfile, getEntityInfo, getFieldInfo, getUdfInfo, lineOfBusiness, profitability | autotask_contract_get |
|
|
1766
1759
|
| `countries` | Country | entity | advanced | no | query, count, get, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_country_get |
|
|
1767
1760
|
| `currencies` | Currency | entity | advanced | no | query, count, get, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_currency_get |
|
|
1768
1761
|
| `departments` | Department | entity | extended | no | query, count, get, create, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_department_get |
|
|
1769
1762
|
| `domain_registrars` | DomainRegistrar | entity | extended | no | query, count, get, create, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_DomainRegistrar_get |
|
|
1770
1763
|
| `expense_item_attachments` | ExpenseItemAttachment | entity | extended | yes (expense_items) | query, count, get, create, delete, resolve | download, getEntityInfo, getFieldInfo, getUdfInfo | autotask_expenseItemAttachment_get |
|
|
1771
|
-
| `expense_items` | ExpenseItem | entity | extended | yes (expense_reports) | query, count, get, create, update, resolve |
|
|
1764
|
+
| `expense_items` | ExpenseItem | entity | extended | yes (expense_reports) | query, count, get, create, update, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_expenseItem_get |
|
|
1772
1765
|
| `expense_reports` | ExpenseReport | entity | extended | no | query, count, get, create, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_expenseReport_get |
|
|
1773
|
-
| `holiday_sets` | HolidaySet | entity | advanced | no | query, count, get, create, update, delete, resolve |
|
|
1774
|
-
| `holidays` | Holiday | entity | advanced | yes (holiday_sets) | query, count, get, create, update, delete, resolve |
|
|
1766
|
+
| `holiday_sets` | HolidaySet | entity | advanced | no | query, count, get, create, update, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_holidaySet_get |
|
|
1767
|
+
| `holidays` | Holiday | entity | advanced | yes (holiday_sets) | query, count, get, create, update, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_holiday_get |
|
|
1775
1768
|
| `invoice_templates` | InvoiceTemplate | entity | advanced | no | query, count, get | getEntityInfo, getFieldInfo, getUdfInfo | autotask_invoiceTemplate_get |
|
|
1776
|
-
| `invoices` | Invoice | entity | extended | no | query, count, get, update, resolve |
|
|
1769
|
+
| `invoices` | Invoice | entity | extended | no | query, count, get, update, resolve | getEntityInfo, getFieldInfo, getUdfInfo, markupHtml, markupXml, pdf | autotask_invoice_get |
|
|
1777
1770
|
| `notification_history` | NotificationHistory | entity | advanced | no | query, count, get, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_notificationHistory_get |
|
|
1778
|
-
| `opportunities` | Opportunity | entity | extended | no | query, count, get, create, update, resolve |
|
|
1771
|
+
| `opportunities` | Opportunity | entity | extended | no | query, count, get, create, update, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_opportunity_get |
|
|
1779
1772
|
| `opportunity_attachments` | OpportunityAttachment | entity | extended | yes (opportunities) | query, count, get, create, delete, resolve | download, getEntityInfo, getFieldInfo, getUdfInfo | autotask_opportunityAttachment_get |
|
|
1780
1773
|
| `opportunity_categories` | OpportunityCategory | entity | extended | no | query, count, get, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_opportunityCategories_get |
|
|
1781
1774
|
| `organizational_level1s` | OrganizationalLevel1 | entity | advanced | no | query, count, get, create, update | getEntityInfo, getFieldInfo, getUdfInfo | autotask_organizationalLevel1_get |
|
|
@@ -1783,7 +1776,7 @@ Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page gu
|
|
|
1783
1776
|
| `organizational_level_associations` | OrganizationalLevelAssociation | entity | advanced | no | query, count, get, create, update | getEntityInfo, getFieldInfo, getUdfInfo | autotask_organizationalLevelAssociation_get |
|
|
1784
1777
|
| `product_vendors` | ProductVendors | entity | extended | yes (products) | query, count, get, create, update, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_productVendor_get |
|
|
1785
1778
|
| `products` | Product | entity | extended | no | query, count, get, create, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_product_get |
|
|
1786
|
-
| `project_charges` | ProjectCharge | entity | extended | yes (projects) | query, count, get, create, update, delete, resolve, search |
|
|
1779
|
+
| `project_charges` | ProjectCharge | entity | extended | yes (projects) | query, count, get, create, update, delete, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_projectCharge_get |
|
|
1787
1780
|
| `project_notes` | ProjectNote | entity | extended | yes (projects) | query, count, get, create, update, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_projectNote_get |
|
|
1788
1781
|
| `phases` | Phase | entity | extended | yes (projects) | query, count, get, create, update, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_phase_get |
|
|
1789
1782
|
| `tasks` | Task | entity | extended | yes (projects) | query, count, get, create, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_task_get |
|
|
@@ -1795,21 +1788,21 @@ Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page gu
|
|
|
1795
1788
|
| `quote_items` | QuoteItem | entity | extended | yes (quotes) | query, count, get, create, update, delete, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_quoteItem_get |
|
|
1796
1789
|
| `quote_locations` | QuoteLocation | entity | extended | yes (quotes) | query, count, get, create, update, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_quoteLocation_get |
|
|
1797
1790
|
| `quote_templates` | QuoteTemplate | entity | extended | no | query, count, get, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_quoteTemplate_get |
|
|
1798
|
-
| `quotes` | Quote | entity | extended | no | query, count, get, create, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_quote_get |
|
|
1791
|
+
| `quotes` | Quote | entity | extended | no | query, count, get, create, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo, withItems | autotask_quote_get |
|
|
1799
1792
|
| `resource_role_queues` | ResourceRoleQueue | entity | extended | yes (resources) | query, count, get, create, update, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_resourceRoleQueue_get |
|
|
1800
1793
|
| `resource_roles` | ResourceRole | entity | extended | yes (resources) | query, count, get, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_resourceRole_get |
|
|
1801
1794
|
| `resource_skills` | ResourceSkill | entity | extended | yes (resources) | query, count, get, update, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_resourceSkill_get |
|
|
1802
1795
|
| `resource_time_off_additional` | ResourceTimeOffAdditional | entity | extended | yes (resources) | update, resolve | getByResource, getEntityInfo, getFieldInfo, getUdfInfo | autotask_resourceTimeOffAdditional |
|
|
1803
1796
|
| `resource_time_off_approvers` | ResourceTimeOffApprover | entity | extended | yes (resources) | query, count, get, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_resourceTimeOffApprover_get |
|
|
1804
1797
|
| `resource_time_off_balances` | ResourceTimeOffBalance | entity | extended | yes (resources) | get, resolve | getByResource, getByYear, getEntityInfo, getFieldInfo, getUdfInfo | autotask_resourceTimeOffBalance_get |
|
|
1805
|
-
| `resources` | Resource | entity | core | no | query, count, get, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo,
|
|
1798
|
+
| `resources` | Resource | entity | core | no | query, count, get, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo, whoAmI, workload | autotask_resource_get |
|
|
1806
1799
|
| `roles` | Role | entity | extended | no | query, count, get, create, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_role_get |
|
|
1807
1800
|
| `sales_orders` | SalesOrder | entity | extended | no | query, count, get, update | getEntityInfo, getFieldInfo, getUdfInfo | autotask_salesOrder_get |
|
|
1808
1801
|
| `service_call_task_resources` | ServiceCallTaskResource | entity | extended | yes (service_call_tasks) | query, count, get, create, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_serviceCallTaskResource_get |
|
|
1809
1802
|
| `service_call_tasks` | ServiceCallTask | entity | extended | yes (service_calls) | query, count, get, create, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_serviceCallTask_get |
|
|
1810
1803
|
| `service_call_ticket_resources` | ServiceCallTicketResource | entity | extended | yes (service_call_tickets) | query, count, get, create, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_serviceCallTicketResource_get |
|
|
1811
1804
|
| `service_call_tickets` | ServiceCallTicket | entity | extended | yes (service_calls) | query, count, get, create, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_serviceCallTicket_get |
|
|
1812
|
-
| `service_calls` | ServiceCall | entity | extended | no | query, count, get, create, update, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_serviceCall_get |
|
|
1805
|
+
| `service_calls` | ServiceCall | entity | extended | no | query, count, get, create, update, delete, resolve | fullProfile, getEntityInfo, getFieldInfo, getUdfInfo | autotask_serviceCall_get |
|
|
1813
1806
|
| `service_level_agreement_results` | ServiceLevelAgreementResults | entity | advanced | no | query, count, get, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_serviceLevelAgreementResults_get |
|
|
1814
1807
|
| `services` | Service | entity | extended | no | query, count, get, create, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_service_get |
|
|
1815
1808
|
| `skills` | Skill | entity | extended | no | query, count, get, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_skill_get |
|
|
@@ -1822,13 +1815,13 @@ Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page gu
|
|
|
1822
1815
|
| `tags` | Tag | entity | advanced | no | query, count, get, create, update, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_tag_get |
|
|
1823
1816
|
| `task_secondary_resources` | TaskSecondaryResource | entity | extended | yes (tasks) | query, count, get, create, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_taskSecondaryResources_get |
|
|
1824
1817
|
| `tax_regions` | TaxRegion | entity | advanced | no | query, count, get, create, update | getEntityInfo, getFieldInfo, getUdfInfo | autotask_taxRegion_get |
|
|
1825
|
-
| `ticket_additional_configuration_items` | TicketAdditionalConfigurationItem | entity | extended | yes (tickets) | query, count, get, create, delete, resolve |
|
|
1826
|
-
| `ticket_additional_contacts` | TicketAdditionalContact | entity | extended | yes (tickets) | query, count, get, create, delete, resolve |
|
|
1818
|
+
| `ticket_additional_configuration_items` | TicketAdditionalConfigurationItem | entity | extended | yes (tickets) | query, count, get, create, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketAdditionalConfigurationItem_get |
|
|
1819
|
+
| `ticket_additional_contacts` | TicketAdditionalContact | entity | extended | yes (tickets) | query, count, get, create, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketAdditionalContact_get |
|
|
1827
1820
|
| `ticket_attachments` | TicketAttachment | entity | extended | yes (tickets) | query, count, get, create, delete, resolve | download, getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketAttachment_get |
|
|
1828
1821
|
| `ticket_categories` | TicketCategory | entity | extended | no | query, count, get, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketCategories_get |
|
|
1829
1822
|
| `ticket_category_field_defaults` | TicketCategoryFieldDefault | entity | extended | yes (ticket_categories) | query, count, get, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketCategoryFieldDefaults_get |
|
|
1830
1823
|
| `ticket_change_request_approvals` | TicketChangeRequestApproval | entity | extended | yes (tickets) | query, count, get, create, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketChangeRequestApprovals_get |
|
|
1831
|
-
| `ticket_charges` | TicketCharge | entity | extended | yes (tickets) | query, count, get, create, update, delete, resolve, search |
|
|
1824
|
+
| `ticket_charges` | TicketCharge | entity | extended | yes (tickets) | query, count, get, create, update, delete, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo, moveToTicket | autotask_ticketCharge_get |
|
|
1832
1825
|
| `ticket_checklist_items` | TicketChecklistItem | entity | extended | yes (tickets) | query, count, get, create, update, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketChecklistItem_get |
|
|
1833
1826
|
| `ticket_checklist_libraries` | TicketChecklistLibrary | entity | extended | yes (tickets) | create, resolve | attachToDocument, getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketChecklistLibrary |
|
|
1834
1827
|
| `ticket_history` | TicketHistory | entity | extended | no | query, count, get, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketHistory_get |
|
|
@@ -1837,8 +1830,8 @@ Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page gu
|
|
|
1837
1830
|
| `ticket_notes` | TicketNote | entity | extended | yes (tickets) | query, count, get, create, update, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketNote_get |
|
|
1838
1831
|
| `ticket_secondary_resources` | TicketSecondaryResource | entity | extended | yes (tickets) | query, count, get, create, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketSecondaryResources_get |
|
|
1839
1832
|
| `ticket_webhooks` | TicketWebhook | entity | extended | yes (tickets) | query, count, get, create, update, delete, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketWebhook_get |
|
|
1840
|
-
| `tickets` | Ticket | entity | core | no | query, count, get, create, update, resolve, search, getContext | changeStatus, findByTicketNumber, forResource, fullContext, getEntityInfo, getFieldInfo, getUdfInfo, lineOfBusiness,
|
|
1841
|
-
| `time_entries` | TimeEntry | entity | extended | no | query, count, get, create, update, delete, resolve |
|
|
1833
|
+
| `tickets` | Ticket | entity | core | no | query, count, get, create, update, resolve, search, getContext | changeStatus, findByTicketNumber, forResource, fullContext, getEntityInfo, getFieldInfo, getUdfInfo, lineOfBusiness, slaHealthCheck, statusSemantics, summary, timeline | autotask_ticket_get |
|
|
1834
|
+
| `time_entries` | TimeEntry | entity | extended | no | query, count, get, create, update, delete, resolve | dateWorkedWindow, getEntityInfo, getFieldInfo, getUdfInfo, moveToTicket | autotask_timeEntry_get |
|
|
1842
1835
|
| `time_entry_attachments` | TimeEntryAttachment | entity | extended | yes (time_entries) | query, count, get, create, delete, resolve | download, getEntityInfo, getFieldInfo, getUdfInfo | autotask_timeEntryAttachment_get |
|
|
1843
1836
|
| `time_off_requests` | TimeOffRequest | entity | extended | no | query, count, get, resolve | approve, getEntityInfo, getFieldInfo, getUdfInfo, reject | autotask_timeOffRequest_get |
|
|
1844
1837
|
| `inventory_products` | InventoryProduct | entity | extended | no | query, count, get, create, update, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo, stockedItemsAdd, stockedItemsRemove, stockedItemsTransfer | autotask_inventoryProduct_get |
|
|
@@ -1850,7 +1843,7 @@ Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page gu
|
|
|
1850
1843
|
| `threshold_information` | — | integration | core | no | get | — | autotask_threshold_information |
|
|
1851
1844
|
| `zone_information` | — | integration | advanced | no | get | — | autotask_zone_information |
|
|
1852
1845
|
|
|
1853
|
-
|
|
1846
|
+
136 resources (129 entity + 7 integration). Rows are generated from the registry — do not hand-type.
|
|
1854
1847
|
<!-- END GENERATED:api-per-entity -->
|
|
1855
1848
|
|
|
1856
1849
|
**`companies.search` include_family (issue #59).** `companies.search` is
|
|
@@ -1914,15 +1907,14 @@ drift). Records exist only for plan rows that are implemented/tested; the
|
|
|
1914
1907
|
plan (`capabilities.plan.json`) is the single source of truth and the
|
|
1915
1908
|
registry is its compiled form.
|
|
1916
1909
|
|
|
1917
|
-
**
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
`
|
|
1922
|
-
|
|
1910
|
+
**SDK-default window (issues #115/#291).** `contracts.profitability` takes
|
|
1911
|
+
`dateFrom`/`dateTo` as an optional PAIR: both omitted → the window defaults to
|
|
1912
|
+
the last full calendar month (UTC) and the result's `period` carries
|
|
1913
|
+
`defaulted: true` plus a note (month label + UTC basis); exactly one supplied
|
|
1914
|
+
→ a typed `CONFIG_ERROR` before any wire; both supplied → the existing
|
|
1915
|
+
validation. The consumer no longer fills the window — the served
|
|
1923
1916
|
`metadata.args`/`metadata.usage` and the record's
|
|
1924
|
-
`inputSchema.opts.description` state the
|
|
1925
|
-
raw manifest should apply the same contract.
|
|
1917
|
+
`inputSchema.opts.description` state the SDK default.
|
|
1926
1918
|
|
|
1927
1919
|
### 9.1 Generated artifact #5: `src/relationships.ts` (the relationship map + zone facts)
|
|
1928
1920
|
|
|
@@ -1947,11 +1939,11 @@ cannot drift from the plan):
|
|
|
1947
1939
|
describe channel cannot drift.
|
|
1948
1940
|
- `DELETE_BOUNDARY_NOTES` (issue #9) — resource → the spec-derived
|
|
1949
1941
|
delete-boundary note (`plan.deleteBoundaries`): the create dry run's
|
|
1950
|
-
`delete-boundary` warning
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
the model has none).
|
|
1942
|
+
`delete-boundary` warning reads the map (the op that shared it,
|
|
1943
|
+
`operations.recovery`, was deleted in #289 — the dry-run and describe
|
|
1944
|
+
channels cannot drift).
|
|
1945
|
+
- `MARKER_FIELDS` (issue #9) — resource → the spec-derived name-like marker
|
|
1946
|
+
field (the plan's `markerField`; absent when the model has none).
|
|
1955
1947
|
- `EMPTY_REASON_STAMPS` (issue #14) — resource → op → the typed empty-reason
|
|
1956
1948
|
`{ surface, kind, note, authority, remedy }` (`plan.emptyReasons`, the
|
|
1957
1949
|
ww29-sweep-pinned 184 rows): the runtime lookup `base.ts` attaches to
|
|
@@ -1973,10 +1965,10 @@ The per-resource entry also carries `deleteBoundary`
|
|
|
1973
1965
|
|
|
1974
1966
|
Consumers: the runtime sentinel wrap (T10), `operations.relationshipMap`
|
|
1975
1967
|
(S12-B — the `autotask_relationship_map` tool serves `RELATIONSHIP_MAP`
|
|
1976
|
-
zero-wire by construction)
|
|
1977
|
-
`
|
|
1978
|
-
|
|
1979
|
-
|
|
1968
|
+
zero-wire by construction) and the create dry run (§7.5). Drift-guarded:
|
|
1969
|
+
`capabilities:check` fails when the artifact bytes ≠ a fresh re-derivation
|
|
1970
|
+
of the plan (`test/scripts/relationships-artifact.test.ts` pins it
|
|
1971
|
+
TDD-style).
|
|
1980
1972
|
## 10. Response redaction (secret and personal fields)
|
|
1981
1973
|
|
|
1982
1974
|
The SDK boundary redacts response field values. A vendor 2xx JSON body is
|
|
@@ -2061,8 +2053,9 @@ resource paths, of which **40** expose a create op (the stampable set; the
|
|
|
2061
2053
|
13-record live-verified set above stays a distinct channel, `proof:
|
|
2062
2054
|
'live-verified'`). The new `plan.deleteBoundaries` block (spec-derived,
|
|
2063
2055
|
`proof: 'spec-derived'`) stamps each affected create's descriptor
|
|
2064
|
-
(`metadata.deleteBoundary`)
|
|
2065
|
-
|
|
2056
|
+
(`metadata.deleteBoundary`) and the create dry run (§7.5) read the SAME
|
|
2057
|
+
generated note (the one-call recovery op that shared it was deleted in
|
|
2058
|
+
#289) — see §11.9.
|
|
2066
2059
|
|
|
2067
2060
|
### 11.2 Blind count/query surfaces (get is the authority)
|
|
2068
2061
|
|
|
@@ -2108,7 +2101,7 @@ resource paths, of which **40** expose a create op (the stampable set; the
|
|
|
2108
2101
|
type itself is discoverable via `configuration_item_types.query` (the wall is the
|
|
2109
2102
|
FK, not the type).
|
|
2110
2103
|
|
|
2111
|
-
### 11.4 `time_entries.
|
|
2104
|
+
### 11.4 `time_entries.create` — zone facts
|
|
2112
2105
|
|
|
2113
2106
|
- The vendor **double-validates roleID** (an active general allocation code AND
|
|
2114
2107
|
a Role reference); in ww29 the two sets are disjoint, so the task/ticket
|
|
@@ -2241,13 +2234,12 @@ of the data).
|
|
|
2241
2234
|
everything — the sentinel notes, the record stamps, the generated
|
|
2242
2235
|
`src/relationships.ts` (§9.1) and this section.
|
|
2243
2236
|
- **S12 additions (PLANNED — land with the A/B workstreams, not yet on
|
|
2244
|
-
main)**:
|
|
2237
|
+
main)**: new registry ops — `tickets.openWithNoteAndAssign` /
|
|
2245
2238
|
`tickets.changeStatus` / `tickets.fullContext` · `contacts.upsertByEmail` ·
|
|
2246
2239
|
`companies.upsertByName` / `companies.fullProfile` ·
|
|
2247
|
-
`configuration_items.profile` · `operations.
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
coordinator step.
|
|
2240
|
+
`configuration_items.profile` · `operations.relationshipMap` (serves the
|
|
2241
|
+
§9.1 artifact zero-wire). Each gets a curation row in
|
|
2242
|
+
`MCP_TOOL_OVERRIDES.json` at the post-merge coordinator step.
|
|
2251
2243
|
|
|
2252
2244
|
### 11.8 Live-validated create examples + the `createIfNotExists` dedupFields shapes
|
|
2253
2245
|
|
|
@@ -2346,11 +2338,13 @@ treat `entry.evidence` with the same trust you already apply to
|
|
|
2346
2338
|
`vendorEvidence` on this surface (plan-grade provenance, not model-facing
|
|
2347
2339
|
prose). The old `string[]` shape is gone.
|
|
2348
2340
|
|
|
2349
|
-
### 11.9 Delete-boundary awareness
|
|
2341
|
+
### 11.9 Delete-boundary awareness (issue #9)
|
|
2350
2342
|
|
|
2351
2343
|
The §11.1 wall (7 live-verified 405 endpoints) is now a **spec-derived
|
|
2352
2344
|
channel** that covers the whole surface, not just the 13 live-verified
|
|
2353
|
-
records. The
|
|
2345
|
+
records. The one-call recovery op that turned this boundary into a typed,
|
|
2346
|
+
bounded path was deleted in #289 (no server consumer) — the channel below
|
|
2347
|
+
is the plan data + the create dry run that remain. The audit (`scripts/lib/delete-boundary.mjs` — the same shared lib
|
|
2354
2348
|
the S11-F stamp channel and the F2 manifest note use) re-derives the
|
|
2355
2349
|
vendored spec's DELETE-absence set, and the plan carries it as a NEW top-level
|
|
2356
2350
|
block (distinct from `plan.blindSurfaces`, which the 6-pin set-equality gate
|
|
@@ -2366,27 +2360,25 @@ keeps untouched):
|
|
|
2366
2360
|
`autotask_describe` surfaces it before the write).
|
|
2367
2361
|
- **generated runtime data** — `src/relationships.ts` emits
|
|
2368
2362
|
`DELETE_BOUNDARY_NOTES` (resource → note) + `MARKER_FIELDS` (resource → the
|
|
2369
|
-
spec-derived name-like field
|
|
2370
|
-
|
|
2363
|
+
spec-derived name-like marker field, 22 pins) from the same plan (zero
|
|
2364
|
+
wire by construction).
|
|
2371
2365
|
- **the create dry run** (`src/resources/base.ts`) — on a delete-less
|
|
2372
2366
|
resource the envelope gains the report-only `delete-boundary` check
|
|
2373
2367
|
(`ok: true`) + the boundary note in `warnings[]`. The check is
|
|
2374
2368
|
report-only on purpose: the write itself is valid, so `wouldApply` is
|
|
2375
2369
|
preserved (the design's `ok: false` example would have broken the pinned
|
|
2376
2370
|
`companies`/`tickets` create-dry-run contract).
|
|
2377
|
-
-
|
|
2378
|
-
turns the boundary from a wall into a typed, bounded path: find the record
|
|
2379
|
-
a write just created (one bounded marker query), and where the vendor
|
|
2380
|
-
allows it, remove it (dry-run-first, confirm-gated apply, per-id
|
|
2381
|
-
verification). The three new typed errors (`DELETE_BOUNDARY`,
|
|
2371
|
+
- **typed errors** — the three issue-#9 refusal codes (`DELETE_BOUNDARY`,
|
|
2382
2372
|
`MARKER_UNSAFE_ID`, `RECOVERY_TRUNCATED`) + the reused `RESOURCE_UNKNOWN`
|
|
2383
|
-
|
|
2384
|
-
silent
|
|
2385
|
-
|
|
2386
|
-
|
|
2373
|
+
stay in the write-outcome taxonomy (`src/errors/mutation.ts`) after #289
|
|
2374
|
+
deleted the op that threw them: never a raw 405, never a silent skip,
|
|
2375
|
+
never a silent empty.
|
|
2376
|
+
- **the gate** (`scripts/check-capabilities.mjs`) — the channel has its own
|
|
2377
|
+
set-equality clause: the block must equal a fresh spec re-derivation
|
|
2387
2378
|
over the stampable set, and EXACTLY the pinned create records carry
|
|
2388
2379
|
`metadata.deleteBoundary` (content match). Pinned by
|
|
2389
|
-
`test/
|
|
2380
|
+
`test/scripts/check-capabilities.test.ts` (the F5 clauses) + the
|
|
2381
|
+
full-surface governor suite.
|
|
2390
2382
|
|
|
2391
2383
|
### 11.10 Typed empty reasons — `emptyReason` (issue #14)
|
|
2392
2384
|
|
|
@@ -2467,14 +2459,17 @@ blind) return `{ count, emptyReason? }` on a 0 — check
|
|
|
2467
2459
|
existing contract, now with the reason alongside).
|
|
2468
2460
|
|
|
2469
2461
|
|
|
2470
|
-
### 11.11
|
|
2462
|
+
### 11.11 The zero-wire blind-surface + reachability plan data (issue #11)
|
|
2471
2463
|
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
14), so an agent can read "what is blind on
|
|
2475
|
-
budget or guessing
|
|
2464
|
+
The campaign's own discovery data ships as plan data (ZERO vendor calls —
|
|
2465
|
+
`plan.reachability` is derived into `src/relationships.ts`, drift-guarded
|
|
2466
|
+
by `capabilities:check` clause 14), so an agent can read "what is blind on
|
|
2467
|
+
this zone" without burning budget or guessing. The two ops that served it
|
|
2468
|
+
at runtime (`operations.blindSurfaces`, `operations.probeReachability`)
|
|
2469
|
+
were deleted in #289 (no server consumer) — the data and its derivation
|
|
2470
|
+
remain:
|
|
2476
2471
|
|
|
2477
|
-
- **`
|
|
2472
|
+
- **`BLIND_SURFACE_INVENTORY`** — the 124-surface inventory: per surface the
|
|
2478
2473
|
kind (`unowned-blind` / `unfiltered-blind` / `unknown` — the 46
|
|
2479
2474
|
data-with-rows surfaces ride `unknown` by design: the sweep's limit-2 read
|
|
2480
2475
|
proves data, not the absence of it), the recorded `unfilteredRows` (0-2
|
|
@@ -2485,14 +2480,13 @@ budget or guessing:
|
|
|
2485
2480
|
note. Clause 14 pins the row set equal to the plan's query-able resources,
|
|
2486
2481
|
the counts derived from the rows, and the row-level kind invariant (a
|
|
2487
2482
|
blind-class row must have 0 recorded rows).
|
|
2488
|
-
- **`
|
|
2489
|
-
(
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
from a blind surface proves nothing — the inventory's kind says why).
|
|
2483
|
+
- **`REACHABILITY_PROBE_DEFAULTS`** — the derived probe defaults: the hard
|
|
2484
|
+
wire cap (80), the default cap (40), the limit bounds (1-2, default 2)
|
|
2485
|
+
and the default target set — the derived non-data-full rows (the 70
|
|
2486
|
+
surfaces a re-discovery sweep starts from: the 69 blind +
|
|
2487
|
+
`ticket_history`). Derived zero-wire; the gate re-derives the target set,
|
|
2488
|
+
so it cannot drift from the inventory. (A 0 from a blind surface proves
|
|
2489
|
+
nothing — the inventory's kind says why.)
|
|
2496
2490
|
|
|
2497
2491
|
### 11.12 Zone-fact create guidance (issue #12)
|
|
2498
2492
|
|
|
@@ -2639,7 +2633,7 @@ metadata is (the `autotask_describe` channel, `capabilities.json`,
|
|
|
2639
2633
|
key. Unlike `pointerNote`, caveats ride the describe channel only (they are
|
|
2640
2634
|
not attached to error details).
|
|
2641
2635
|
|
|
2642
|
-
### 11.13 Posted state from billing items (W-D3), `
|
|
2636
|
+
### 11.13 Posted state from billing items (W-D3), `dateWorkedWindow`, and drifted-create marker recovery (issues #10/#13/#237)
|
|
2643
2637
|
|
|
2644
2638
|
- **W-D3 (live, ww29, re-verified 2026-10-27)** — this zone's TimeEntry entity
|
|
2645
2639
|
has **no posted-state field**: time-entry reads carry no `postedDate` /
|
|
@@ -2659,51 +2653,39 @@ metadata is (the `autotask_describe` channel, `capabilities.json`,
|
|
|
2659
2653
|
condition is ever sent. The join is one `BillingItems` query per 250 ids;
|
|
2660
2654
|
the entry read is capped at 500, so a call costs at most 1 read + 2 joins
|
|
2661
2655
|
and a window with more than 500 entries returns the first 500 with
|
|
2662
|
-
`
|
|
2656
|
+
`truncated` true and a `nextCursor` to continue from (or use
|
|
2663
2657
|
`countOnly` for the window's totals; see the paging bullet below). A 429 or vendor fault propagates as a typed
|
|
2664
2658
|
error. The API user therefore needs BillingItems read permission; a permission fault propagates as a typed error.
|
|
2665
|
-
- **`time_entries.
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
`
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
`
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
`
|
|
2695
|
-
the totals. `time_entries.dateWorkedWindow(from, to, opts?)` is the
|
|
2696
|
-
unambiguous-named alias: the same wire, the same result (incl. `truncated`),
|
|
2697
|
-
its own op identity (plan row / MCP tool
|
|
2698
|
-
`autotask_timeEntry_date_worked_window` / operation label) — the name
|
|
2699
|
-
names the `dateWorked` field the window is on; `postedWithin` keeps its
|
|
2700
|
-
name and default semantics. Both accept `opts.tz` (an IANA zone, validated)
|
|
2701
|
-
but, since 0.24.0, it never shifts the bounds: `dateWorked` is a date-only
|
|
2702
|
-
value at 00:00 UTC, so `from`/`to` are the days (see §7.11). The same `tz`
|
|
2703
|
-
opt is on every date-window op — see §11.22.
|
|
2704
|
-
- **Paging past 500 entries and whole-window totals (issue #242)** — all four
|
|
2705
|
-
ops (`getPosted`, `getUnposted`, `postedWithin`, `dateWorkedWindow`) take
|
|
2706
|
-
three more options:
|
|
2659
|
+
- **`time_entries.dateWorkedWindow(from, to, opts?)`** — the ONE posted-state read
|
|
2660
|
+
(issue #290; absorbs `getPosted` / `getUnposted` / `postedWithin` and the
|
|
2661
|
+
relative windows). `from`/`to` are required inclusive UTC `dateWorked` days
|
|
2662
|
+
(`YYYY-MM-DD` date strings — the guard forbids Date positionals and refuses
|
|
2663
|
+
impossible days), one bounded read (`limit` 1-500, default 500 — the hard cap,
|
|
2664
|
+
issue #128: a larger window needs `time_entries.query` with the same
|
|
2665
|
+
`dateWorked` filter and a suitable `limit` or explicit `walk.maxRows` cap,
|
|
2666
|
+
section 4) returning
|
|
2667
|
+
`{ items, postedCount, unpostedCount, window, nextCursor, unconfirmed, truncated }`;
|
|
2668
|
+
the counts come from the billing-item join (any billing item referencing the
|
|
2669
|
+
entry) and split every entry read (before the narrowing), so `items.length`
|
|
2670
|
+
can differ from their sum; `opts.state` (`'posted'` / `'unposted'`, omitted =
|
|
2671
|
+
both) narrows `items` client-side after the join, and `ticketID` / `resourceID`
|
|
2672
|
+
narrow the read. The no-window form (a call without dates reading the
|
|
2673
|
+
tenant's first 500 time entries, effectively the oldest) is gone: a read with
|
|
2674
|
+
no `dateWorked` filter goes through `time_entries.query`. Live evidence for
|
|
2675
|
+
the visibility class (unfiltered query 0 rows, windowed read has data) from
|
|
2676
|
+
2026-10-06 still stands; the posted/unposted split from that period does not.
|
|
2677
|
+
- **`truncated` (issue #66, consolidated in #290)** — `truncated` is true when
|
|
2678
|
+
the bounded read came back with a live `pageDetails.nextPageUrl` (more
|
|
2679
|
+
matching rows exist; `requestCount` is the MaxRecords echo, not a total, so
|
|
2680
|
+
it cannot carry this signal) or a join page was truncated — the
|
|
2681
|
+
posted/unposted counts then cover the returned rows ONLY (at most `limit`);
|
|
2682
|
+
re-query with a wider window or a narrowing filter to reconcile the totals.
|
|
2683
|
+
The `tz` option is gone with the consolidation (`dateWorked` is a date-only
|
|
2684
|
+
value at 00:00 UTC — the `from`/`to` days ARE the days, never shifted; see
|
|
2685
|
+
§7.11), and the `postedWithin` / `dateWorkedWindow` alias pair is one op now
|
|
2686
|
+
(plan row / MCP tool `autotask_timeEntry_date_worked_window`).
|
|
2687
|
+
- **Paging past 500 entries and whole-window totals (issue #242)** —
|
|
2688
|
+
`dateWorkedWindow` takes three more options:
|
|
2707
2689
|
- `cursor?: string` — every rows result now carries `nextCursor: string | null`
|
|
2708
2690
|
(the vendor's next-page cursor for the entry read). Pass it back as
|
|
2709
2691
|
`opts.cursor` with the **same** window and filters to read the next page;
|
|
@@ -2714,12 +2696,11 @@ metadata is (the `autotask_describe` channel, `capabilities.json`,
|
|
|
2714
2696
|
continuation (`/V1.0/TimeEntries/query/next`), or a cursor longer than
|
|
2715
2697
|
2,048 characters, is refused (`RESPONSE_REJECTED`) before any request. Every rows result also carries
|
|
2716
2698
|
`unconfirmed: number`: the entries on this page whose billing-item join
|
|
2717
|
-
page was truncated. They are withheld from `items`
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
above 0 `hasMore` / `truncated` is also true. Sum it across a loop to know
|
|
2699
|
+
page was truncated. They are withheld from a `state`-narrowed `items`
|
|
2700
|
+
(with `state` omitted they stay in `items`, unclassified) and from both
|
|
2701
|
+
counts, so a cursor loop does not see them as posted or unposted;
|
|
2702
|
+
`unconfirmed` is 0 normally, and when it is above 0 `truncated` is also
|
|
2703
|
+
true. Sum it across a loop to know
|
|
2723
2704
|
how many entries went unclassified (or use `countOnly`, which refuses
|
|
2724
2705
|
instead).
|
|
2725
2706
|
- `countOnly?: true` — returns only the whole window's totals:
|
|
@@ -2735,7 +2716,7 @@ metadata is (the `autotask_describe` channel, `capabilities.json`,
|
|
|
2735
2716
|
and dropped another); a total equal to either count is accepted, so a
|
|
2736
2717
|
window that grows or shrinks by the time the walk ends still succeeds, and
|
|
2737
2718
|
one that changes mid-walk can fail or pass depending on timing: retry.
|
|
2738
|
-
`limit` and `
|
|
2719
|
+
`limit` and `state` do not apply; `cursor` is rejected with `countOnly`
|
|
2739
2720
|
(CONFIG_ERROR).
|
|
2740
2721
|
- `maxVendorCalls?: number` — the `countOnly` budget (integer 1-200,
|
|
2741
2722
|
default 30). The estimate is `1 + max(1, ceil(n / 500)) + ceil(n / 250) + 1`
|
|
@@ -2821,7 +2802,7 @@ resources, `include: ['fields']`; the closed opts bag is documented in
|
|
|
2821
2802
|
the registry record). One bounded entityInformation GET per resource ×
|
|
2822
2803
|
kind; the wire cost `resources × include` is REFUSED pre-request above
|
|
2823
2804
|
the pinned cap (`MAX_FIELD_CATALOG_WIRE = 60`) — refused, never
|
|
2824
|
-
truncated (the
|
|
2805
|
+
truncated (the bounded sweep contract). Per-resource vendor
|
|
2825
2806
|
failures are recorded on the row (`error: { code, message }`) and never
|
|
2826
2807
|
abort the sweep. Capability is PLAN-DERIVED, never hardcoded: the
|
|
2827
2808
|
`getFieldInfo` / `getUdfInfo` row sets and each resource's vendor path
|
|
@@ -3334,57 +3315,35 @@ call.
|
|
|
3334
3315
|
Pins: `test/operations/expandResourceNames.test.ts` (the plan row's `tests[]`)
|
|
3335
3316
|
and `test/resources/cache-write-invalidation.test.ts` (mock wire only).
|
|
3336
3317
|
|
|
3337
|
-
### 11.22
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
invoices
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
dropped the first day and added an extra day; zones ahead of UTC returned the
|
|
3367
|
-
right rows by accident). A zone-converted instant window picks the wrong day
|
|
3368
|
-
behind UTC. DST is
|
|
3369
|
-
handled by inverting local midnights to UTC instants (a local day that
|
|
3370
|
-
gains or loses an hour still pins both boundaries to its TRUE midnights —
|
|
3371
|
-
never clock-hour arithmetic); `last24h` is rolling (`now − 24h → now`)
|
|
3372
|
-
and TZ-invariant by construction. A zone `Intl.DateTimeFormat` rejects is
|
|
3373
|
-
refused pre-request as a typed `CONFIG_ERROR` (zero wire). There is no
|
|
3374
|
-
config-level timezone in v1 (the zone is per-call; a future
|
|
3375
|
-
`config.timeZone` default would be an additive follow-up).
|
|
3376
|
-
- **The existing family gains the same opt** — the 12 `time_entries`
|
|
3377
|
-
window ops (incl. `customRange`) plus `postedWithin` and its
|
|
3378
|
-
`dateWorkedWindow` alias accept `opts.tz`
|
|
3379
|
-
with the contract above (date-only for time entries), so the whole window family accepts
|
|
3380
|
-
the same `tz` option (default UTC unchanged).
|
|
3381
|
-
- **Shape and wire** — each op is exactly ONE bounded query (`MaxRecords`
|
|
3382
|
-
1-500, default 500 — the hard cap, issue #128: a call without `limit` takes
|
|
3383
|
-
the first 500 rows in id order; a window with more than 500 matching rows
|
|
3384
|
-
needs the bounded query op with the same date-window filter — its 2500-row
|
|
3385
|
-
walk cap; the `itemDate` / `invoiceDateTime` `gte`/`lte` filter rides the
|
|
3386
|
-
wire), and the result is the plain `items[]` array — no counts
|
|
3387
|
-
(the time-entry family shape).
|
|
3318
|
+
### 11.22 The cut date-window families — `dateWorkedWindow` and bounded queries (issue #66, consolidated in #290)
|
|
3319
|
+
|
|
3320
|
+
Issue #66 made the window zone EXPLICIT (a `tz` option, default UTC) and
|
|
3321
|
+
mirrored the time-entry window family onto `billing_items` (`itemDate`) and
|
|
3322
|
+
`invoices` (`invoiceDateTime`) — twelve relative ops each. The consolidation
|
|
3323
|
+
(issue #290) cut all of them: ONLY the `time_entries` family is consolidated
|
|
3324
|
+
into the single op `time_entries.dateWorkedWindow(from, to, opts?)` (§11.13)
|
|
3325
|
+
— an inclusive, UTC-anchored `dateWorked` window with the posted/unposted
|
|
3326
|
+
split. The `billing_items` and `invoices` date-window families are CUT with
|
|
3327
|
+
no replacement op: reading those resources by date window goes through their
|
|
3328
|
+
bounded `query` ops on `itemDate` / `invoiceDateTime` (bullet below). The
|
|
3329
|
+
`tz` option is gone (`dateWorked` is a date-only value at 00:00 UTC; the
|
|
3330
|
+
`from`/`to` days ARE the wire bounds, never shifted, and impossible days are
|
|
3331
|
+
refused pre-request).
|
|
3332
|
+
|
|
3333
|
+
- **Reading billing items / invoices by date window** — use the bounded `query`
|
|
3334
|
+
ops: `billing_items.query` with an `itemDate` `gte`/`lte` filter on whole UTC
|
|
3335
|
+
days (`POST /V1.0/BillingItems/query`) and `invoices.query` with
|
|
3336
|
+
`invoiceDateTime` (`POST /V1.0/Invoices/query` — the invoices model has no
|
|
3337
|
+
plain `invoiceDate`; `fromDate`/`toDate` are range fields, not the invoice
|
|
3338
|
+
date). Each call is ONE bounded query (`MaxRecords` 1-500, default 500 — the
|
|
3339
|
+
hard cap, issue #128), the plain `items[]` result, no counts.
|
|
3340
|
+
- **The zone contract (history)** — before #290 the relative windows
|
|
3341
|
+
(`today`, `lastFullMonth`, ...) computed the calendar dates in `opts.tz` and
|
|
3342
|
+
sent whole UTC days (DST handled by inverting local midnights to UTC instants;
|
|
3343
|
+
`last24h` was rolling, `now − 24h → now`, TZ-invariant by construction); a
|
|
3344
|
+
zone the vendor's `Intl.DateTimeFormat` rejects was refused pre-request as a
|
|
3345
|
+
typed `CONFIG_ERROR` (zero wire). No relative-window op remains, so the
|
|
3346
|
+
contract only matters for historical receipts.
|
|
3388
3347
|
|
|
3389
3348
|
### 11.23 Free-text caps (issue #61)
|
|
3390
3349
|
|
|
@@ -3600,8 +3559,8 @@ returns the contract's reconciled P&L over a date window:
|
|
|
3600
3559
|
(ticket `createDate`) as UTC instants, and bucketing slices the ISO
|
|
3601
3560
|
string — in a UTC+10/11 tenant a ticket created late local time buckets
|
|
3602
3561
|
one UTC day (possibly one month) earlier than its local date. The
|
|
3603
|
-
|
|
3604
|
-
|
|
3562
|
+
posted-state ops are gone (cut in #290 — the profitability fold
|
|
3563
|
+
already holds the billing items, so the join is
|
|
3605
3564
|
in-memory with no extra wire); posted/unposted derives from the
|
|
3606
3565
|
`billingItems.timeEntryID` join (unposted = no billing item exists yet).
|
|
3607
3566
|
The structural exclusions (`/Services/`, `/ExpenseReports/`,
|
|
@@ -3682,7 +3641,7 @@ returns the contract's reconciled P&L over a date window:
|
|
|
3682
3641
|
SERVICE lines)` (labour and charges never mask a missed recurring
|
|
3683
3642
|
invoice) · `unposted_time { entries, hours, hoursAbsent?, value, absent? }`
|
|
3684
3643
|
(billable time with no billing item yet — derived from the billing join,
|
|
3685
|
-
never
|
|
3644
|
+
never a posted-state op (cut in #290); `hours` = Σ `max(0, hoursToBill)`, or `null`
|
|
3686
3645
|
+ `hoursAbsent: 'HOURS_TO_BILL_MISSING'` when any of the month's unposted
|
|
3687
3646
|
entries carries no numeric `hoursToBill` — unknown, never a summed 0; a
|
|
3688
3647
|
deliberate `hoursToBill: 0` stays 0; `value` = Σ `hoursToBill ×
|
|
@@ -3865,8 +3824,8 @@ returns the contract's reconciled P&L over a date window:
|
|
|
3865
3824
|
### 11.27 Label siblings on query, aggregate and fetch (opt-in, issue #287)
|
|
3866
3825
|
|
|
3867
3826
|
Rows served by the invoke `<resource>.query` op (the bounded query), the group
|
|
3868
|
-
keys of `operations.aggregate`, and the found records of `operations.
|
|
3869
|
-
|
|
3827
|
+
keys of `operations.aggregate`, and the found records of `operations.fetchByIds`
|
|
3828
|
+
carry label siblings **when opted in** (issue #287:
|
|
3870
3829
|
OFF by default — the label pass costs a field-catalog read plus up to 20 cold
|
|
3871
3830
|
name reads per call, so a plain read returns raw ids), through the same
|
|
3872
3831
|
`resolveCompositeNames` path the full-context composites use:
|
|
@@ -3875,9 +3834,9 @@ name reads per call, so a plain read returns raw ids), through the same
|
|
|
3875
3834
|
siblings on for every read surface of that client (the MCP server sets it,
|
|
3876
3835
|
server-side).
|
|
3877
3836
|
- **The per-call key:** `names: true` in the query bag / the aggregate opts /
|
|
3878
|
-
the `
|
|
3879
|
-
|
|
3880
|
-
|
|
3837
|
+
the `fetchByIds` opts turns them on for that call; `names: false` turns them
|
|
3838
|
+
off (even when the client has `labels: true`); ABSENT = the client's option
|
|
3839
|
+
(the off default). A non-boolean `names` is refused
|
|
3881
3840
|
pre-request.
|
|
3882
3841
|
|
|
3883
3842
|
| field on the row | sibling | example |
|
|
@@ -3940,9 +3899,8 @@ name reads per call, so a plain read returns raw ids), through the same
|
|
|
3940
3899
|
per-call cap of 20 uncached name reads was hit — names already read are
|
|
3941
3900
|
cached, so repeating or narrowing the call labels more"`. Unresolvable
|
|
3942
3901
|
picklists add a note clause and a `reasons` entry per field (the
|
|
3943
|
-
field-catalog honesty note / the value-not-served note); on `
|
|
3944
|
-
|
|
3945
|
-
field name. Absent `names` = every sibling was resolved.
|
|
3902
|
+
field-catalog honesty note / the value-not-served note); on `fetchByIds`
|
|
3903
|
+
the notes merge across resources and `reasons` keys the plain field name. Absent `names` = every sibling was resolved.
|
|
3946
3904
|
- Response size (full model, wrapped names): a tickets row grows ~40% (live,
|
|
3947
3905
|
100 rows, 31 siblings per row, some of them null: 341,178 B -> 477,325 B;
|
|
3948
3906
|
more when every FK is populated), a time_entries row ~77% (worst case, every
|
|
@@ -4011,6 +3969,35 @@ truncates first, then wraps) — so the explicit `… [+N chars truncated]`
|
|
|
4011
3969
|
marker survives INSIDE the wrapped value and the closing marker is never
|
|
4012
3970
|
cut off.
|
|
4013
3971
|
|
|
3972
|
+
**`sanitiseResult(client, result, operation)`** exposes that field projection
|
|
3973
|
+
as a zero-wire operations helper. Pass the registry key of the read that
|
|
3974
|
+
produced the result, for example `tickets.query`; an unknown key is a typed
|
|
3975
|
+
`CONFIG_ERROR`. It walks plain objects and arrays, wrapping strings in the
|
|
3976
|
+
operation's `metadata.untrustedFields`, including strings in arrays under
|
|
3977
|
+
those fields. `userDefinedFields[].value` strings are fenced even when the
|
|
3978
|
+
operation has no field stamp. Deeply nested containers are capped at 64
|
|
3979
|
+
levels with `[DEPTH]`.
|
|
3980
|
+
Inputs are never mutated, wrapping is idempotent, and non-string values
|
|
3981
|
+
and `[REDACTED]` pass through. SDK read primitives still return raw values.
|
|
3982
|
+
|
|
3983
|
+
This helper supplies field/UDF fencing. The host retains text cleaning,
|
|
3984
|
+
free-text caps and trusted formatting exemptions; apply those policies in
|
|
3985
|
+
the host before projection where needed. The bounded-query `noteChars`
|
|
3986
|
+
option remains a separate SDK cap applied before wrapping.
|
|
3987
|
+
|
|
3988
|
+
For hosts that need to clean or cap text before wrapping, the
|
|
3989
|
+
`node-autotask/untrusted` subpath exports
|
|
3990
|
+
`deepMarkUntrusted(value, fields, projector?)` and the callback type
|
|
3991
|
+
`UntrustedStringProjector = (text: string, target: object, key: string | number) => string`.
|
|
3992
|
+
The optional callback runs only for marked string values, including UDF
|
|
3993
|
+
values and strings in named arrays. `target` is the copied output
|
|
3994
|
+
container; `key` is its property name or array index, assigned after the
|
|
3995
|
+
callback returns. A host can record provenance in a `WeakMap` keyed by
|
|
3996
|
+
`target` and return the raw text, then clean, cap and wrap those marked
|
|
3997
|
+
values later. Containers exposed to the callback remain copies even if
|
|
3998
|
+
the callback returns unchanged text. Unmarked strings and non-string
|
|
3999
|
+
values do not invoke it; omitting the callback keeps the default wrapping.
|
|
4000
|
+
|
|
4014
4001
|
## 13. Doctor preflight (issue #2)
|
|
4015
4002
|
|
|
4016
4003
|
`runDoctor(client)` (section 1.10) runs the bounded, sequential preflight
|