stitchkit 0.118.1 → 0.119.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 +66 -0
- package/dist/browser/http.d.ts.map +1 -1
- package/dist/contract/client-types.d.ts +6 -3
- package/dist/contract/client-types.d.ts.map +1 -1
- package/dist/contract/define-validation.d.ts.map +1 -1
- package/dist/contract/define.d.ts +18 -12
- package/dist/contract/define.d.ts.map +1 -1
- package/dist/contract/endpoint-capabilities.d.ts +28 -0
- package/dist/contract/endpoint-capabilities.d.ts.map +1 -0
- package/dist/contract/factory.d.ts +49 -19
- package/dist/contract/factory.d.ts.map +1 -1
- package/dist/contract/tool-view.d.ts +1 -1
- package/dist/contract/tool-view.d.ts.map +1 -1
- package/dist/contract/transports.d.ts +5 -0
- package/dist/contract/transports.d.ts.map +1 -0
- package/dist/entrypoints/agent-runtime/coding-tools.js +6 -5
- package/dist/entrypoints/agent-runtime/harness.js +6 -5
- package/dist/entrypoints/agent-runtime/sandbox.js +6 -5
- package/dist/entrypoints/agent-runtime/sqlite/bun.js +9 -8
- package/dist/entrypoints/agent-runtime/sqlite/node.js +9 -8
- package/dist/entrypoints/agent-runtime/testing.js +9 -8
- package/dist/entrypoints/agent-runtime.js +14 -13
- package/dist/entrypoints/application/diagnostic-journal.js +3 -3
- package/dist/entrypoints/application/directory-inbox.js +4 -4
- package/dist/entrypoints/application/opentelemetry.js +1 -1
- package/dist/entrypoints/application.js +5 -5
- package/dist/entrypoints/cli/publish.js +4 -4
- package/dist/entrypoints/cli.js +8 -7
- package/dist/entrypoints/contract.d.ts +1 -1
- package/dist/entrypoints/contract.d.ts.map +1 -1
- package/dist/entrypoints/contract.js +6 -4
- package/dist/entrypoints/files.js +3 -3
- package/dist/entrypoints/geo/contract.d.ts +7 -0
- package/dist/entrypoints/geo/contract.d.ts.map +1 -0
- package/dist/entrypoints/geo/contract.js +6 -0
- package/dist/entrypoints/geo.js +60 -23
- package/dist/entrypoints/http.js +2 -2
- package/dist/entrypoints/index.js +8 -6
- package/dist/entrypoints/live.js +1 -1
- package/dist/entrypoints/node.js +7 -6
- package/dist/entrypoints/primitives.js +1 -1
- package/dist/entrypoints/process/owner-loss.js +1 -1
- package/dist/entrypoints/process.js +3 -3
- package/dist/entrypoints/react.js +1 -1
- package/dist/entrypoints/remote.js +4 -3
- package/dist/entrypoints/server.js +11 -10
- package/dist/entrypoints/telegram.js +2 -2
- package/dist/entrypoints/testing.js +7 -6
- package/dist/entrypoints/tools/connections.js +3 -2
- package/dist/entrypoints/tools/contract.js +1 -1
- package/dist/entrypoints/tools/invoker.js +5 -4
- package/dist/entrypoints/tools/mcp.js +7 -6
- package/dist/entrypoints/tools.js +17 -15
- package/dist/entrypoints/tracking/contract.js +5 -3
- package/dist/entrypoints/tracking/performance.js +3 -3
- package/dist/entrypoints/tracking.d.ts +1 -1
- package/dist/entrypoints/tracking.d.ts.map +1 -1
- package/dist/entrypoints/tracking.js +63 -83
- package/dist/entrypoints/xlsx.js +1 -1
- package/dist/geo/index.d.ts +2 -1
- package/dist/geo/index.d.ts.map +1 -1
- package/dist/geo/record.d.ts +8 -0
- package/dist/geo/record.d.ts.map +1 -0
- package/dist/geo/resolver.d.ts +1 -3
- package/dist/geo/resolver.d.ts.map +1 -1
- package/dist/geo/schema.d.ts +44 -0
- package/dist/geo/schema.d.ts.map +1 -0
- package/dist/geo/types.d.ts +2 -13
- package/dist/geo/types.d.ts.map +1 -1
- package/dist/{index-n5sv4z0w.js → index-1k18qzqa.js} +13 -15
- package/dist/{index-tsngj2b5.js → index-2dfvv1jg.js} +206 -206
- package/dist/{index-d9hq291n.js → index-4jpykzvw.js} +70 -81
- package/dist/{index-ghtpfz4z.js → index-57gqaq8b.js} +2 -2
- package/dist/index-69t14s4a.js +39 -0
- package/dist/{index-6p3frkv5.js → index-6a69xzrx.js} +4 -4
- package/dist/{index-dky0dcmf.js → index-72r9k0av.js} +6 -2
- package/dist/{index-vv1sch8a.js → index-8vzh5wpv.js} +13 -8
- package/dist/{index-xx1j310k.js → index-8yxgcyt6.js} +171 -169
- package/dist/{index-p6apg0jg.js → index-9frf01t9.js} +16 -9
- package/dist/{index-q0th8q0z.js → index-9mjrnqxs.js} +1 -3
- package/dist/{index-ega3yz4c.js → index-aqwg2dm5.js} +1 -1
- package/dist/{index-073nw9qr.js → index-ba7are04.js} +524 -522
- package/dist/{index-095ya5hm.js → index-bbvc59gw.js} +13 -11
- package/dist/{index-txg1npmg.js → index-cyxp8rbf.js} +1 -1
- package/dist/index-d3xdyjt6.js +38 -0
- package/dist/{index-n8z3rvzw.js → index-f4njz9j0.js} +3 -5
- package/dist/{index-90p34rts.js → index-fcqj44mb.js} +3 -1
- package/dist/{index-86nbfysb.js → index-ff39f9dr.js} +3 -3
- package/dist/{index-75ns2ybj.js → index-fqd4yqwd.js} +1 -1
- package/dist/{index-nqxrm176.js → index-g444vfz2.js} +1 -1
- package/dist/{index-vwhrnp67.js → index-kz9734jc.js} +3 -3
- package/dist/{index-dr3t5n1v.js → index-nn9qvkww.js} +46 -46
- package/dist/{index-qezhmp51.js → index-nz06qw5a.js} +3 -1
- package/dist/{index-1bx1xv1k.js → index-pfvde2fz.js} +5 -2
- package/dist/{index-s80awwsw.js → index-qch1vcaa.js} +3 -1
- package/dist/{index-cycp3r3e.js → index-rxcgp4b9.js} +2 -2
- package/dist/{index-w7jq3fz9.js → index-s0qjs6ee.js} +1198 -1196
- package/dist/{index-p0rf7sq9.js → index-s20s2mad.js} +209 -209
- package/dist/{index-v6vb5qx8.js → index-s3yxw6kt.js} +1 -1
- package/dist/{index-cqmqyjax.js → index-sa99p98w.js} +1 -1
- package/dist/{index-0gxvns6s.js → index-sba3e6m4.js} +1 -0
- package/dist/{index-f24bdt13.js → index-v1swr26p.js} +3 -1
- package/dist/{index-rhzkndr5.js → index-x4p3kw1p.js} +533 -530
- package/dist/{index-hdjmdwsj.js → index-x8d9fdeg.js} +513 -507
- package/dist/{index-htxh46ej.js → index-ycqhyars.js} +2 -2
- package/dist/{index-9bwcthxw.js → index-yj6saztf.js} +31 -13
- package/dist/{index-tdpmfks8.js → index-ynrf908v.js} +1 -1
- package/dist/{index-f6m4c545.js → index-zrdcmg6j.js} +4 -6
- package/dist/process/bun-child.d.ts +3 -2
- package/dist/process/bun-child.d.ts.map +1 -1
- package/dist/server/contract-method.d.ts.map +1 -1
- package/dist/tools/internal/surface-projector.d.ts.map +1 -1
- package/dist/tools/operations/async-operation-canonical-types.d.ts +1 -1
- package/dist/tracking/attribution.d.ts +21 -25
- package/dist/tracking/attribution.d.ts.map +1 -1
- package/dist/tracking/client-visit.d.ts +5 -7
- package/dist/tracking/client-visit.d.ts.map +1 -1
- package/dist/tracking/client.d.ts +2 -2
- package/dist/tracking/client.d.ts.map +1 -1
- package/dist/tracking/contract.d.ts +1 -1
- package/dist/tracking/host.d.ts +0 -1
- package/dist/tracking/host.d.ts.map +1 -1
- package/dist/tracking/privacy.d.ts +6 -0
- package/dist/tracking/privacy.d.ts.map +1 -1
- package/dist/tracking/schemas.d.ts +2 -2
- package/dist/tracking/schemas.d.ts.map +1 -1
- package/llms/contract.txt +46 -24
- package/llms/geo-contract.txt +23 -0
- package/llms/geo.txt +40 -4
- package/llms/stitchkit.txt +4 -3
- package/llms/tools.2.txt +2 -2
- package/llms/tools.3.txt +1 -1
- package/llms/tracking-contract.txt +1 -1
- package/llms/tracking.txt +27 -5
- package/llms/{upgrading-0.105.0-0.118.0.txt → upgrading-0.106.0-0.119.0.txt} +121 -410
- package/llms/upgrading-0.37.0-0.53.0.txt +969 -0
- package/llms/{upgrading-0.44.0-0.62.0.txt → upgrading-0.55.0-0.68.0.txt} +509 -528
- package/llms/{upgrading-0.63.0-0.83.0.txt → upgrading-0.69.0-0.95.0.txt} +506 -509
- package/llms/{upgrading-0.84.0-0.104.0.txt → upgrading-0.96.0-0.105.0.txt} +410 -506
- package/llms/upgrading.txt +5 -5
- package/llms-full.txt +247 -38
- package/llms.txt +10 -9
- package/package.json +23 -19
- package/llms/upgrading-0.37.0-0.44.0.txt +0 -444
|
@@ -1,14 +1,132 @@
|
|
|
1
|
-
# Upgrading 0.
|
|
1
|
+
# Upgrading 0.106.0 → 0.119.0 — stitchkit docs slice
|
|
2
2
|
|
|
3
3
|
> Generated from the `docs/` tree by `bun run gen:llms`. The index of all slices is
|
|
4
4
|
> `llms.txt` in this package.
|
|
5
5
|
|
|
6
|
-
Contains: Guide: Upgrading, 0.
|
|
6
|
+
Contains: Guide: Upgrading, 0.106.0 → 0.119.0 (docs/guide/upgrading.md)
|
|
7
7
|
|
|
8
8
|
==============================================================================
|
|
9
|
-
# Guide: Upgrading, 0.
|
|
9
|
+
# Guide: Upgrading, 0.106.0 → 0.119.0 (docs/guide/upgrading.md)
|
|
10
10
|
==============================================================================
|
|
11
11
|
|
|
12
|
+
## Released migration: 0.119.0
|
|
13
|
+
|
|
14
|
+
### factory-default-exposure
|
|
15
|
+
|
|
16
|
+
**Who must act:** code passing arguments to `createContractFactory`, depending on its fixed-string
|
|
17
|
+
policy types or the renamed exposure types, calling HTTP clients of endpoints typed as the broad
|
|
18
|
+
`EndpointDef` / `FactoryScopedEndpoint`, or declaring multipart endpoints with non-HTTP exposure or
|
|
19
|
+
tool options.
|
|
20
|
+
|
|
21
|
+
Scope and default transports are inferred separately. Replace the constructor configuration:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// before
|
|
25
|
+
const { defineContract } = createContractFactory<AppScope>({ toolExposure: 'explicit' })
|
|
26
|
+
// after: preserve the same curated HTTP-only default
|
|
27
|
+
const { defineContract } = createContractFactory<AppScope>().configure({
|
|
28
|
+
toolExposure: { default: ['HTTP'] },
|
|
29
|
+
})
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
To deliberately expose ordinary operations through HTTP and CLI, choose `default: ['HTTP', 'CLI']`
|
|
33
|
+
and remove redundant matching endpoint arrays. Explicit arrays replace, rather than extend, the
|
|
34
|
+
default. Raw responses, multipart, raw bodies, response metadata, response streams and HEAD get only
|
|
35
|
+
HTTP from implicit defaults; a CLI-only or empty default leaves them hidden on every transport.
|
|
36
|
+
Explicit contradictions fail at definition; remove non-HTTP transports and tool options from multipart
|
|
37
|
+
endpoints rather than relying on their silent omission from tool catalogs.
|
|
38
|
+
|
|
39
|
+
`ExplicitToolExposureEndpoints` and `ExplicitScopedDefineContract` are renamed
|
|
40
|
+
`ConfiguredToolExposureEndpoints<Endpoints, Defaults>` and `ConfiguredScopedDefineContract<Scope, Defaults>`:
|
|
41
|
+
they describe materialization from any default tuple, their second generic, and HTTP-only when it is
|
|
42
|
+
omitted.
|
|
43
|
+
An optional or conditional endpoint override remains an alternative in the returned type. It does not
|
|
44
|
+
guarantee an HTTP client method unless every alternative includes HTTP.
|
|
45
|
+
|
|
46
|
+
HTTP clients and URL builders no longer assume a widened `Transport[]` contains
|
|
47
|
+
HTTP. Pass the literal array directly to `configure` when the HTTP surface is static;
|
|
48
|
+
its const generic infers the tuple. An annotation or `satisfies ContractFactoryConfig`
|
|
49
|
+
on a separately stored configuration does not preserve the tuple by itself.
|
|
50
|
+
Optional capabilities likewise retain the filtered alternative in endpoint types.
|
|
51
|
+
|
|
52
|
+
The rule reads the endpoint's type, with or without a factory. An endpoint returned by a helper
|
|
53
|
+
annotated `(): EndpointDef` or `(): FactoryScopedEndpoint<Scope>`, or holding a stored
|
|
54
|
+
`readonly Transport[]`, has no HTTP client method or URL, because that type allows an exposure
|
|
55
|
+
without HTTP. Keep the literal type with a const generic helper:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
// before
|
|
59
|
+
const read = (): FactoryScopedEndpoint<AppScope> => ({ method: 'GET', path: '/', desc: 'Read', output })
|
|
60
|
+
// after
|
|
61
|
+
const endpoint = <const Endpoint extends FactoryScopedEndpoint<AppScope>>(value: Endpoint) => value
|
|
62
|
+
const read = () => endpoint({ method: 'GET', path: '/', desc: 'Read', output })
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
If every endpoint already has an explicit array, removing the constructor configuration alone also
|
|
66
|
+
preserves exposure. [ADR 0283](../decisions/0283-factory-default-exposure-is-materialized-by-capability.md).
|
|
67
|
+
|
|
68
|
+
Search patterns: `createContractFactory`, `toolExposure`, `ExplicitToolExposureEndpoints`,
|
|
69
|
+
`ExplicitScopedDefineContract`, `: EndpointDef`, `: FactoryScopedEndpoint`, and multipart endpoints
|
|
70
|
+
with `expose` or `tool`.
|
|
71
|
+
|
|
72
|
+
### geo-full-snapshot
|
|
73
|
+
|
|
74
|
+
**Who must act:** code comparing `stitchkit/geo` results by equality, parsing them with a strict
|
|
75
|
+
schema of its own, mutating a returned `GeoAttribution`, or relying on empty labels.
|
|
76
|
+
|
|
77
|
+
The default mapper returns everything the City + ASN record states: continent, the registered
|
|
78
|
+
country as its own fields, GeoNames identifiers, `accuracyRadiusKm` and `localizedNames` with every
|
|
79
|
+
language. A record that used to give `{ city: 'Bangkok' }` now also gives
|
|
80
|
+
`localizedNames: { city: { en: 'Bangkok' } }`.
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
// before: an application's copy of the shape
|
|
84
|
+
const StoredGeo = z.strictObject({ city: z.string().optional(), countryCode: z.string().optional() })
|
|
85
|
+
// after: the framework's schema, importable from shared code
|
|
86
|
+
import { GeoAttributionSchema } from 'stitchkit/geo/contract'
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`GeoAttribution` is readonly to its nested dictionaries; copy before changing a field. Empty labels
|
|
90
|
+
and coordinates outside ±90° / ±180° are omitted. The located country never borrows the registered
|
|
91
|
+
one; write `countryCode ?? registeredCountryCode` where a country for every address is wanted.
|
|
92
|
+
|
|
93
|
+
Search pattern: `stitchkit/geo`, `mapGeoIpRecord`, `GeoAttribution`, `resolve(`.
|
|
94
|
+
|
|
95
|
+
### raw-tracking-attribution
|
|
96
|
+
|
|
97
|
+
**Who must act:** code passing `referrerMap` or `hostname` to `resolveAttribution`, importing
|
|
98
|
+
`ReferrerRule` or `parseReferrer`, relying on inferred, lowercased or retained UTM, or reading `utm`
|
|
99
|
+
from an event batch on the server.
|
|
100
|
+
|
|
101
|
+
Tracking sends raw attribution facts. Remove `referrerMap`, `ReferrerRule`, `parseReferrer` and the
|
|
102
|
+
resolver's `hostname`; `TrackingPageContext` and `browserTrackingHost()` no longer require or return
|
|
103
|
+
`hostname` either. Classify `referrer` and campaign tags on your server instead. Configure the
|
|
104
|
+
click-marker names your application recognises:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
// before
|
|
108
|
+
createTrackingClient({ ...config, referrerMap: [{ pattern: /search\.example/, source: 'search', medium: 'organic' }] })
|
|
109
|
+
// after
|
|
110
|
+
createTrackingClient({ ...config, clickIdParams: ['gclid', 'msclkid'] })
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Bootstrap and both touches carry `clickId`, the configured parameter's name without its value. Extend
|
|
114
|
+
your bootstrap DTO or stored attribution shape if you previously copied the framework's fields. No
|
|
115
|
+
marker is recognised unless configured. Real UTM retains its case and whitespace, and a tag without
|
|
116
|
+
`utm_source` is no longer discarded. Keep any channel normalisation in the server classifier.
|
|
117
|
+
|
|
118
|
+
`currentTouch` is the current capture's context; an untagged entry has no campaign even when a prior
|
|
119
|
+
one did. Implement last-non-direct retention on the server if required. First-touch storage uses a
|
|
120
|
+
versioned format and resets unversioned records once, because inferred and real tags in those records
|
|
121
|
+
cannot be distinguished. Without browser storage, current landing facts still reach bootstrap.
|
|
122
|
+
|
|
123
|
+
An event batch (`TrackEventsRequest`, the `request` schema of `createTrackingSchemas`) carries
|
|
124
|
+
`buildId` and `events` only. A server that read `request.utm` attributes events through their visit:
|
|
125
|
+
store the bootstrap entry's `utm`, `referrer` and `clickId` with the visit and join on `visitId`.
|
|
126
|
+
|
|
127
|
+
Search pattern: `referrerMap`, `ReferrerRule`, `parseReferrer`, `resolveAttribution`, `currentTouch`,
|
|
128
|
+
`request.utm`, `input.utm`.
|
|
129
|
+
|
|
12
130
|
## Released migration: 0.118.0
|
|
13
131
|
|
|
14
132
|
### telegram-due-cursor
|
|
@@ -480,410 +598,3 @@ as `{ kind: 'stopped', cause, exitCode, signal }`, with how the kernel reported
|
|
|
480
598
|
3. A hook that stops an external scope on a stopped command now runs once the leader is gone,
|
|
481
599
|
still bounded by `cleanupTimeoutMs`; no change is needed unless the leader waited for that scope
|
|
482
600
|
to exit (it gets KILL when the grace ends either way).
|
|
483
|
-
|
|
484
|
-
## Released migration: 0.105.0
|
|
485
|
-
|
|
486
|
-
### CLI publication has its own entrypoint
|
|
487
|
-
|
|
488
|
-
**Who must act:** code that imports `publishCli`, `CliPublicationOptions`, `CliPublicationPhase` or
|
|
489
|
-
`CliPublicationResult` from `stitchkit/cli`. Everything else in `stitchkit/cli` — `createCli`, the
|
|
490
|
-
manifest schemas, signatures, `renderCliInstaller` and the updater — is unchanged.
|
|
491
|
-
|
|
492
|
-
`publishCli` is release infrastructure, so it now ships from the evolving entrypoint
|
|
493
|
-
`stitchkit/cli/publish` instead of the stable `stitchkit/cli`
|
|
494
|
-
([ADR 0245](../decisions/0245-cli-publication-is-an-evolving-leaf.md)). Its options, result and
|
|
495
|
-
behaviour are the same.
|
|
496
|
-
|
|
497
|
-
1. Move the publication names to the new specifier; keep every other CLI import where it is:
|
|
498
|
-
|
|
499
|
-
```ts
|
|
500
|
-
// before
|
|
501
|
-
import { publishCli, type CliPublicationOptions, verifyCliManifest } from 'stitchkit/cli'
|
|
502
|
-
// after
|
|
503
|
-
import { verifyCliManifest } from 'stitchkit/cli'
|
|
504
|
-
import { publishCli, type CliPublicationOptions } from 'stitchkit/cli/publish'
|
|
505
|
-
```
|
|
506
|
-
|
|
507
|
-
Search pattern: `\b(publishCli|CliPublication(Options|Phase|Result))\b` in files that import
|
|
508
|
-
from `'stitchkit/cli'`.
|
|
509
|
-
2. Type-check: an import left on `stitchkit/cli` fails with "has no exported member".
|
|
510
|
-
3. `stitchkit/cli/publish` is evolving: read its breaking entries in each minor you upgrade across.
|
|
511
|
-
|
|
512
|
-
### a command stops by its `stop` policy
|
|
513
|
-
|
|
514
|
-
**Who must act:** callers of `runNativeCommand` (`stitchkit/process`) that pass `killGraceMs`.
|
|
515
|
-
Callers that omit it need no change: the default stop is the same TERM to the group, 100 ms, KILL.
|
|
516
|
-
|
|
517
|
-
1. Replace `killGraceMs: N` with a group stop of the same grace:
|
|
518
|
-
|
|
519
|
-
```ts
|
|
520
|
-
// before
|
|
521
|
-
await runNativeCommand({ executable, args, signal, killGraceMs: 500 })
|
|
522
|
-
// after
|
|
523
|
-
await runNativeCommand({ executable, args, signal, stop: { target: 'group', graceMs: 500 } })
|
|
524
|
-
```
|
|
525
|
-
|
|
526
|
-
Search pattern: `killGraceMs:\s*(\d[\d_]*)` → `stop: { target: 'group', graceMs: $1 }`.
|
|
527
|
-
2. If your own wrapper signals only the leader and waits a long grace (cooperative cancellation),
|
|
528
|
-
replace it with `stop: { target: 'leader', signal: 'SIGINT', graceMs }` (up to one hour). Pass
|
|
529
|
-
`killOn` with your shutdown signal so an application that stops sooner kills the group first.
|
|
530
|
-
3. The result of a stopped command rejects once the group is gone, up to `graceMs` plus
|
|
531
|
-
`cleanupTimeoutMs` after the abort; size your caller deadlines on that sum.
|
|
532
|
-
|
|
533
|
-
### a command stops what its leader left
|
|
534
|
-
|
|
535
|
-
**Who must act:** callers of `runNativeCommand` (`stitchkit/process`) whose command starts a helper
|
|
536
|
-
in its own process group that must keep running after the command returns. A daemon that detaches
|
|
537
|
-
with `setsid` (as `daemon(3)` does) is not in the group and needs
|
|
538
|
-
no change.
|
|
539
|
-
|
|
540
|
-
`descendants` now defaults to `'terminate-after-leader'`
|
|
541
|
-
([ADR 0246](../decisions/0246-a-command-stops-its-descendants-by-default.md)): when the leader
|
|
542
|
-
exits, the members it left in the group are stopped by the command's `stop` policy before the
|
|
543
|
-
pipes drain.
|
|
544
|
-
|
|
545
|
-
1. Find the commands that start a background helper on purpose (a `&` in a shell script, a spawned
|
|
546
|
-
child the leader does not wait for) and declare `'leave'` on them:
|
|
547
|
-
|
|
548
|
-
```ts
|
|
549
|
-
// before
|
|
550
|
-
await runNativeCommand({ executable, args, timeoutMs })
|
|
551
|
-
// after
|
|
552
|
-
await runNativeCommand({ executable, args, timeoutMs, descendants: 'leave' })
|
|
553
|
-
```
|
|
554
|
-
|
|
555
|
-
2. Remove an explicit `descendants: 'terminate-after-leader'`, or keep it: it is now the default.
|
|
556
|
-
3. A command whose helper inherited stdout or stderr used to wait for that helper until its
|
|
557
|
-
deadline. It now completes when the leader exits; drop a workaround that killed the helper from
|
|
558
|
-
`onLeaderSettled`.
|
|
559
|
-
|
|
560
|
-
### native packaging verifies published digests
|
|
561
|
-
|
|
562
|
-
**Who must act:** build scripts that call `createNativePackaging` (`stitchkit/files/packaging`) and
|
|
563
|
-
read `asset.sourcePath`, hash the addon themselves, switch over the refusal `state`/`code`, pass a
|
|
564
|
-
`platform` other than the literal `'darwin'`, or parse `node_modules/stitchkit/native-assets.json`.
|
|
565
|
-
|
|
566
|
-
The package now publishes the `size` and SHA256 of each Darwin addon, computed when Stitchkit is
|
|
567
|
-
built, in `native-assets.json` at `formatVersion: 2`. `createNativePackaging` reads each selected
|
|
568
|
-
addon once, compares it with that digest and returns the verified bytes. An installed addon that
|
|
569
|
-
differs refuses with `NATIVE_ASSET_DIGEST_MISMATCH`.
|
|
570
|
-
|
|
571
|
-
1. Replace every read of `asset.sourcePath` with `asset.bytes`, and delete your own re-hash of the
|
|
572
|
-
addon: the result's `sha256` is the published digest and `bytes` already match it.
|
|
573
|
-
|
|
574
|
-
```ts
|
|
575
|
-
// before
|
|
576
|
-
const bytes = readFileSync(asset.sourcePath)
|
|
577
|
-
if (createHash('sha256').update(bytes).digest('hex') !== asset.sha256) throw …
|
|
578
|
-
copyFileSync(asset.sourcePath, destination)
|
|
579
|
-
// after
|
|
580
|
-
writeFileSync(destination, asset.bytes)
|
|
581
|
-
```
|
|
582
|
-
|
|
583
|
-
2. Handle the third refusal. A `switch` over `result.state` gains `'mismatch'`
|
|
584
|
-
(`code: 'NATIVE_ASSET_DIGEST_MISMATCH'`); treat it as a failed build, never as a missing addon.
|
|
585
|
-
It carries the differing `architecture` with `expected` (published) and `actual` (installed)
|
|
586
|
-
`{ size, sha256 }`; log both pairs.
|
|
587
|
-
3. Pass the target platform as the literal `'darwin'`. `platform` is a closed enum, so a free
|
|
588
|
-
string (a target name read from configuration, `process.platform`) no longer type-checks, and
|
|
589
|
-
another platform throws instead of returning `unsupported`. Skip packaging for non-Darwin
|
|
590
|
-
targets before the call:
|
|
591
|
-
|
|
592
|
-
```ts
|
|
593
|
-
// before
|
|
594
|
-
const native = createNativePackaging({ platform: target.platform, architecture, … })
|
|
595
|
-
if (native.state === 'unsupported') return
|
|
596
|
-
// after
|
|
597
|
-
if (target.platform !== 'darwin') return
|
|
598
|
-
const native = createNativePackaging({ platform: target.platform, architecture, … })
|
|
599
|
-
```
|
|
600
|
-
|
|
601
|
-
`unsupported` now means only an architecture the package has no addon for.
|
|
602
|
-
4. Call `createNativePackaging` once per build and pass the result (its `assets` and `plugin`)
|
|
603
|
-
where it is needed. Each call reads and hashes the selected addons again.
|
|
604
|
-
5. If you read `native-assets.json` directly, read `assets.<arch>.path`, `.size` and `.sha256`
|
|
605
|
-
instead of a bare path per architecture; an architecture with no entry is not published.
|
|
606
|
-
|
|
607
|
-
### exclusive locks publish their owner first
|
|
608
|
-
|
|
609
|
-
**Who must act:** callers of `withExclusiveLock` (`stitchkit/files`) that rely on a lock file with no
|
|
610
|
-
readable owner being taken by age, and anything that puts lock files on a filesystem without hard
|
|
611
|
-
links. Every other caller needs no change.
|
|
612
|
-
|
|
613
|
-
A lock is now written to a private `.lock-<id>.tmp` file in the lock's directory, then hard-linked to
|
|
614
|
-
the lock name. The name never exists without a complete owner record, so `ownerlessGraceMs` defaults
|
|
615
|
-
to `null` instead of 5 000 ms: a lock file with no readable owner is never taken because of its age.
|
|
616
|
-
|
|
617
|
-
1. If you depended on age-based reclaim of an empty lock file left by an older writer, opt in:
|
|
618
|
-
|
|
619
|
-
```ts
|
|
620
|
-
// before: an empty lock older than 5 s was taken by default
|
|
621
|
-
await withExclusiveLock(path, run)
|
|
622
|
-
// after: say so; only legacy empty files need it
|
|
623
|
-
await withExclusiveLock(path, run, { ownerlessGraceMs: 5_000 })
|
|
624
|
-
```
|
|
625
|
-
|
|
626
|
-
2. Keep lock directories on a filesystem that supports hard links (local ext4, xfs, APFS and the
|
|
627
|
-
like). `link` failing with `EPERM` or `ENOTSUP` fails the acquisition.
|
|
628
|
-
3. Expect a `.lock-*.tmp` sibling only after a holder was killed between writing its record and
|
|
629
|
-
publishing it. It is not a lock; the next reclaim in that directory removes the ones whose owner
|
|
630
|
-
is gone on this machine. Exclude the pattern from directory-size caps or backups if you have any.
|
|
631
|
-
|
|
632
|
-
### one runtime-tool declaration
|
|
633
|
-
|
|
634
|
-
**Who must act:** code that names `RuntimeToolExecution`, `RuntimeToolExecutionWithOutput`,
|
|
635
|
-
`RuntimeMcpToolDefinition`, `RuntimeMcpToolDefinitionWithOutput` or `ToolSurfaceDefinition`; code
|
|
636
|
-
that annotates a definition carrying `present` as `RuntimeToolDefinitionWithOutput<I, O>` from
|
|
637
|
-
`stitchkit/tools`; imports of `stitchkit/agent-runtime/harness-tools`; code that compares agent
|
|
638
|
-
control error codes; and callers that send a JSON array as text to a string-or-array tool field.
|
|
639
|
-
Definitions returned by `defineRuntimeTool` / `createRuntimeToolFactory`, native CLI `commands` and
|
|
640
|
-
contract tools need no change.
|
|
641
|
-
|
|
642
|
-
A runtime tool now has one declaration, `RuntimeToolDefinitionWithOutput` /
|
|
643
|
-
`RuntimeToolDefinitionWithoutOutput`, exported with the same name from `stitchkit/cli`,
|
|
644
|
-
`stitchkit/tools` and `stitchkit/tools/mcp`. It carries no SDK type: the presenters are the adapter's
|
|
645
|
-
extension, named as the fourth type argument. `RuntimeToolDefinition` is the one registration type.
|
|
646
|
-
|
|
647
|
-
1. Replace each removed name.
|
|
648
|
-
|
|
649
|
-
| before | after |
|
|
650
|
-
|---|---|
|
|
651
|
-
| `RuntimeToolExecutionWithOutput<I, O>` (`stitchkit/cli`) | `RuntimeToolDefinitionWithOutput<I, O>` |
|
|
652
|
-
| `RuntimeToolExecution` (`stitchkit/cli`) | `RuntimeToolDefinition` |
|
|
653
|
-
| `RuntimeMcpToolDefinitionWithOutput<I, O>` (`stitchkit/tools/mcp`) | `RuntimeToolDefinitionWithOutput<I, O, undefined, RuntimeMcpToolPresenters<z.output<O>>>` |
|
|
654
|
-
| `RuntimeMcpToolDefinition` (`stitchkit/tools/mcp`) | `RuntimeToolDefinition` |
|
|
655
|
-
| `ToolSurfaceDefinition` (`stitchkit/tools`) | `ToolSurfaceProjection<RuntimeToolDefinition>` |
|
|
656
|
-
|
|
657
|
-
2. In `stitchkit/tools`, a type that names a definition with presenters passes the presenter type:
|
|
658
|
-
|
|
659
|
-
```ts
|
|
660
|
-
// before
|
|
661
|
-
const tool: RuntimeToolDefinitionWithOutput<typeof input, typeof output> = defineRuntimeTool({ …, present })
|
|
662
|
-
// after: let defineRuntimeTool infer it, or name the presenters
|
|
663
|
-
const tool = defineRuntimeTool({ …, present })
|
|
664
|
-
const named: RuntimeToolDefinitionWithOutput<typeof input, typeof output, undefined, RuntimeToolPresenters<z.output<typeof output>>> = tool
|
|
665
|
-
```
|
|
666
|
-
|
|
667
|
-
3. An inline handler in `runtimeTools: [...]` needs no wrapper: it is typed by the parsed object.
|
|
668
|
-
To type `input` from the schema, construct with `defineRuntimeTool` (or
|
|
669
|
-
`satisfies RuntimeToolDefinitionWithOutput<typeof input, typeof output>`) before registering.
|
|
670
|
-
|
|
671
|
-
4. Replace `createAgentHarnessTools` (`stitchkit/agent-runtime/harness-tools`) with the call it made:
|
|
672
|
-
|
|
673
|
-
```ts
|
|
674
|
-
// before
|
|
675
|
-
tools: createAgentHarnessTools(({ context }) => ({ services: [svc], context, lifecycle: auth }))
|
|
676
|
-
// after
|
|
677
|
-
import { composeToolLifecycle, mountAgent } from 'stitchkit/tools'
|
|
678
|
-
tools: ({ context, toolFenceLifecycle }) =>
|
|
679
|
-
mountAgent([svc], { context, lifecycle: composeToolLifecycle(auth, toolFenceLifecycle) })
|
|
680
|
-
```
|
|
681
|
-
|
|
682
|
-
5. Agent control codes: `ACCESS_DENIED` is `FORBIDDEN`, and `CONNECTION_CLOSED` is returned for a closed
|
|
683
|
-
connection. `AgentControlErrorCode` lists every code. `authorize` now runs once per request; after you
|
|
684
|
-
withdraw someone's access, call `binding.revoke(conversationId, matches?)` so an open stream is
|
|
685
|
-
re-authorized and ends with an `access-denied` delivery.
|
|
686
|
-
|
|
687
|
-
6. A string-or-array field that receives `'["a","b"]'` now fails with `VALIDATION_ERROR` ("`paths` is a
|
|
688
|
-
list written as text"). Send the array itself. A string that is not a list the array member accepts
|
|
689
|
-
(`[preview].png`) is still one plain string.
|
|
690
|
-
|
|
691
|
-
### flattened unions keep declaration order
|
|
692
|
-
|
|
693
|
-
**Who must act:** consumers that use `flattenUnionInput: true` and keep snapshot tests, golden
|
|
694
|
-
files or provider prompt caches of flattened tool schemas.
|
|
695
|
-
|
|
696
|
-
A flattened union now lists its properties, enum values and branch labels in the order they first
|
|
697
|
-
appear across the variants, not alphabetically. Declaration order often encodes priority, so the
|
|
698
|
-
model now reads the order you wrote. Alternatives of different kinds (`anyOf`) keep their
|
|
699
|
-
canonical order, independent of variant order.
|
|
700
|
-
|
|
701
|
-
1. Regenerate every snapshot or golden file of a flattened tool listing (`buildToolManifest`,
|
|
702
|
-
`listToolNames`, MCP `tools/list`, Agent tool schemas) and review the diff: only ordering moves.
|
|
703
|
-
2. Expect one provider prompt-cache miss per flattened tool after the upgrade.
|
|
704
|
-
3. To keep an order you prefer, declare the variants and fields in that order.
|
|
705
|
-
|
|
706
|
-
### connection limits are validated
|
|
707
|
-
|
|
708
|
-
**Who must act:** callers that pass `timeoutMs` or `maxResponseBytes` to a connection and relied
|
|
709
|
-
on an invalid value silently becoming the default.
|
|
710
|
-
|
|
711
|
-
`stitchkit/tools/connections` throws a `RangeError` when `timeoutMs` is not a positive integer
|
|
712
|
-
within timer range (at most 2 147 483 647) or `maxResponseBytes` is not a positive safe integer.
|
|
713
|
-
Before 0.104.0 a value of 0, a negative number, a fraction or an out-of-range number fell back to
|
|
714
|
-
the default without a message.
|
|
715
|
-
|
|
716
|
-
```ts
|
|
717
|
-
// Before: { timeoutMs: 0 } meant "use the default" by accident.
|
|
718
|
-
// After: omit the option to take the default, or pass a real limit.
|
|
719
|
-
{ timeoutMs: 30_000 }
|
|
720
|
-
```
|
|
721
|
-
|
|
722
|
-
### one canonical JSON serializer
|
|
723
|
-
|
|
724
|
-
**Who must act:** tests that call `serializeSurfaceValue` from `stitchkit/testing`, and code that
|
|
725
|
-
puts a value nested deeper than 100 levels into agent store events, archives or projections
|
|
726
|
-
through `canonicalAgentJson`.
|
|
727
|
-
|
|
728
|
-
`canonicalJson` from `stitchkit/primitives` is the package's one canonical serializer. It
|
|
729
|
-
admits dense arrays, plain objects, strings, booleans, `null` and finite numbers; an `undefined`
|
|
730
|
-
object member is omitted. It refuses everything else with a `CanonicalJsonError`, a `TypeError`
|
|
731
|
-
whose `reason` says why: `'depth'`, `'nodes'` and `'bytes'` are the limits you set, while
|
|
732
|
-
`'cycle'`, `'negative-zero'` and `'not-json'` say the value is not JSON data.
|
|
733
|
-
|
|
734
|
-
`serializeSurfaceValue` is removed. It produced the same bytes without limits:
|
|
735
|
-
|
|
736
|
-
```ts
|
|
737
|
-
// Before
|
|
738
|
-
import { serializeSurfaceValue } from 'stitchkit/testing'
|
|
739
|
-
const text = serializeSurfaceValue(manifest)
|
|
740
|
-
// After: the limits default to 100 levels, 100 000 nodes and 1 MiB; raise them for a large manifest
|
|
741
|
-
import { canonicalJson } from 'stitchkit/primitives'
|
|
742
|
-
const text = canonicalJson(manifest, { maxBytes: 16 * 1024 * 1024 })
|
|
743
|
-
```
|
|
744
|
-
|
|
745
|
-
`canonicalAgentJson` in `stitchkit/agent-runtime` is `canonicalJson` with the node and byte
|
|
746
|
-
limits at their maximum. A value both accepted has the same bytes as before, so stored digests
|
|
747
|
-
and archives still match: like `z.json()` before it, the store hashes `-0` as `0` and drops an
|
|
748
|
-
own `__proto__` member (the public `canonicalJson` refuses `-0` and keeps `__proto__`). What
|
|
749
|
-
changed is the refusal table: an `undefined` object member used to fail and is now omitted, and
|
|
750
|
-
a value nested deeper than 100 levels is now refused.
|
|
751
|
-
|
|
752
|
-
```ts
|
|
753
|
-
// Before: canonicalAgentJson({ a: undefined }) threw a ZodError
|
|
754
|
-
// After: canonicalAgentJson({ a: undefined }) === '{}'
|
|
755
|
-
// canonicalAgentJson(nestedBeyond100Levels) throws CanonicalJsonError (reason 'depth')
|
|
756
|
-
```
|
|
757
|
-
|
|
758
|
-
### directory inbox entries
|
|
759
|
-
|
|
760
|
-
**Who must act:** producers that drop hard links into an inbox directory, and callers that pass
|
|
761
|
-
`accept` the output of a transforming schema.
|
|
762
|
-
|
|
763
|
-
A file dropped into the inbox directory with more than one link is set aside as `invalid`
|
|
764
|
-
(detail `managed file has multiple links`). Another name can still rewrite such a file after it
|
|
765
|
-
was validated, so it is never trusted. Write each entry as its own regular file: a temporary
|
|
766
|
-
name in the same directory, then a rename onto `<name>.json`.
|
|
767
|
-
|
|
768
|
-
`accept` takes the schema's input type, stores that input as given and parses it with the
|
|
769
|
-
schema on delivery. A schema without a transform has the same input and output type and needs
|
|
770
|
-
no change. With a transform, pass the input:
|
|
771
|
-
|
|
772
|
-
```ts
|
|
773
|
-
const schema = z.object({ amount: z.string().transform(Number) })
|
|
774
|
-
// Before: accept({ source, key, entry: { amount: 42 } }) // typed as the output; `z.string()` refused it
|
|
775
|
-
// After: accept({ source, key, entry: { amount: '42' } }) // delivered as { amount: 42 }
|
|
776
|
-
```
|
|
777
|
-
|
|
778
|
-
A name that is already in `rejected/` no longer stops the pass: identical bytes are the entry
|
|
779
|
-
already set aside, different bytes are kept as `rejected/<name>.<16 hex digits>`.
|
|
780
|
-
|
|
781
|
-
### journal startup scan
|
|
782
|
-
|
|
783
|
-
**Who must act:** operators that read `status.recovery` right after `createDiagnosticJournal` to
|
|
784
|
-
find damaged rows in the middle of a file.
|
|
785
|
-
|
|
786
|
-
Opening a journal reads only the last line of the active file and of every retained
|
|
787
|
-
generation (`startupScan: 'tails'`, the default). A crash tears the end of a file, so a torn tail
|
|
788
|
-
is still reported and still rotated away, and opening no longer parses and validates every
|
|
789
|
-
line of every generation. It still reads each file once in large blocks to count lines, so the time
|
|
790
|
-
grows with the bytes retained, at read speed. Pass `startupScan: 'full'` to read and validate every line as before:
|
|
791
|
-
|
|
792
|
-
```ts
|
|
793
|
-
// Before: every open parsed every line of every generation.
|
|
794
|
-
// After: the same full scan, on request.
|
|
795
|
-
const journal = await createDiagnosticJournal({ path, eventSchema, limits, startupScan: 'full' })
|
|
796
|
-
```
|
|
797
|
-
|
|
798
|
-
### journal startup quarantine
|
|
799
|
-
|
|
800
|
-
**Who must act:** operators of a journal that is a source of truth and must not start over
|
|
801
|
-
damaged retained files; code that constructs `DiagnosticJournalRecoveryError` or reads its
|
|
802
|
-
`recovery`; protocols that embed `DiagnosticJournalRecoveryStatusSchema`, and their readers.
|
|
803
|
-
A diagnostic journal needs no change.
|
|
804
|
-
|
|
805
|
-
Opening a journal no longer refuses to start over a torn active file with `maxFiles: 1`, a
|
|
806
|
-
generation name that is not a regular file or a retained file it cannot read. It renames the file
|
|
807
|
-
to `<file>.quarantined-<epoch>` beside the journal, starts, and lists the move in
|
|
808
|
-
`getStatus().recovery.quarantined`. Quarantined files are outside retention and never deleted;
|
|
809
|
-
every later open lists them again, without a `reason`, until you remove them.
|
|
810
|
-
|
|
811
|
-
1. A journal whose damage must stop the process declares the old behaviour:
|
|
812
|
-
|
|
813
|
-
```ts
|
|
814
|
-
// Before: refusing was the only behaviour.
|
|
815
|
-
await createDiagnosticJournal({ path, eventSchema, limits })
|
|
816
|
-
// After:
|
|
817
|
-
await createDiagnosticJournal({ path, eventSchema, limits, onStartupRefusal: 'fail' })
|
|
818
|
-
```
|
|
819
|
-
|
|
820
|
-
2. Branch on the refusal's `reason` instead of its message or error class. Every startup refusal
|
|
821
|
-
about retained evidence is a `DiagnosticJournalRecoveryError`, including the former plain
|
|
822
|
-
`Error` for a non-file generation and the raw `EACCES` for an unreadable one (now in `cause`):
|
|
823
|
-
|
|
824
|
-
```ts
|
|
825
|
-
// Before: catch (error) { if (error instanceof DiagnosticJournalRecoveryError) use(error.recovery) }
|
|
826
|
-
// After:
|
|
827
|
-
catch (error) {
|
|
828
|
-
if (error instanceof DiagnosticJournalRecoveryError && error.reason === 'torn-without-retention-slot') use(error.recovery)
|
|
829
|
-
}
|
|
830
|
-
```
|
|
831
|
-
|
|
832
|
-
`recovery` is present only for `torn-without-retention-slot`. The constructor takes one
|
|
833
|
-
options object: `new DiagnosticJournalRecoveryError({ reason, file, recovery?, cause? })`.
|
|
834
|
-
|
|
835
|
-
3. A protocol that embeds `DiagnosticJournalRecoveryStatusSchema` carries the new optional
|
|
836
|
-
`quarantined` array (at most 32 entries, `reason` optional) and `quarantinedUnlisted` count.
|
|
837
|
-
The schema is `.strict()`: upgrade every reader that parses it with its own copy before a
|
|
838
|
-
writer on this version sends a status with either field, or bump your protocol version.
|
|
839
|
-
|
|
840
|
-
### error constructors take named options
|
|
841
|
-
|
|
842
|
-
**Who must act:** code that constructs an `AppError` or an `ApiError` with anything beyond its
|
|
843
|
-
code, and classes that extend `AppError` and call `super(code, message, status, …)`. Code that
|
|
844
|
-
only throws the `notFound`, `badRequest`, `unauthorized`, `forbidden`, `conflict` and
|
|
845
|
-
`rateLimited` helpers, or only reads `code`, `status`, `details`, `hint`, `traceId`,
|
|
846
|
-
`retryable` and `cause` from a caught error, needs no change.
|
|
847
|
-
|
|
848
|
-
`AppError` (`stitchkit/contract`, `stitchkit/server`, `stitchkit/node`) and `ApiError`
|
|
849
|
-
(`stitchkit`) keep the code as the one positional argument. Every other value is a named
|
|
850
|
-
option, so a call site reads without the signature open. `new AppError(code)` and
|
|
851
|
-
`new ApiError(code)` are unchanged. The compiler finds every old call: a string or number
|
|
852
|
-
in the second position is a type error.
|
|
853
|
-
|
|
854
|
-
1. `AppError`: move `message`, `status`, `details`, `hint`, `traceId` and `retryable` into one
|
|
855
|
-
object after the code. `status` defaults to 500 and `message` to the code.
|
|
856
|
-
|
|
857
|
-
```ts
|
|
858
|
-
// before
|
|
859
|
-
throw new AppError('CONFLICT', 'Selection is stale', 409, { revision: 7 })
|
|
860
|
-
throw new AppError('BUSY', 'Try later', 503, undefined, undefined, undefined, true)
|
|
861
|
-
// after
|
|
862
|
-
throw new AppError('CONFLICT', { message: 'Selection is stale', status: 409, details: { revision: 7 } })
|
|
863
|
-
throw new AppError('BUSY', { message: 'Try later', status: 503, retryable: true })
|
|
864
|
-
```
|
|
865
|
-
|
|
866
|
-
2. A subclass passes the same object to `super`:
|
|
867
|
-
|
|
868
|
-
```ts
|
|
869
|
-
// before
|
|
870
|
-
super('FEATURE_LOCKED', 'locked', 403)
|
|
871
|
-
// after
|
|
872
|
-
super('FEATURE_LOCKED', { message: 'locked', status: 403 })
|
|
873
|
-
```
|
|
874
|
-
|
|
875
|
-
3. `ApiError`: the old order was `code, status, details, message, hint, traceId, errorOptions,
|
|
876
|
-
retryable`. Name each value; `status` defaults to 0 and the former `ErrorOptions` argument
|
|
877
|
-
is now the `cause` option.
|
|
878
|
-
|
|
879
|
-
```ts
|
|
880
|
-
// before
|
|
881
|
-
new ApiError('RATE_LIMITED', 429, undefined, 'slow down', 'wait', 'trace-1', { cause }, true)
|
|
882
|
-
new ApiError('VALIDATION_ERROR', 0, { issues }, 'bad input')
|
|
883
|
-
// after
|
|
884
|
-
new ApiError('RATE_LIMITED', { status: 429, message: 'slow down', hint: 'wait', traceId: 'trace-1', retryable: true, cause })
|
|
885
|
-
new ApiError('VALIDATION_ERROR', { status: 0, details: { issues }, message: 'bad input' })
|
|
886
|
-
```
|
|
887
|
-
|
|
888
|
-
4. Both classes accept the standard `cause`, which `error.cause` returns; omit the option to
|
|
889
|
-
leave the property absent.
|