@makaio/extension-artifact-patch 1.0.0-dev-1789118938035 → 1.0.0-dev-1789148673705

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/README.md CHANGED
@@ -25,7 +25,9 @@ of what was asked — is worse than a rejected one:
25
25
  addressed position, applying the change to the rest would report a partial write as a complete
26
26
  one. A missing intermediate is `PATH_NOT_RESOLVABLE`; a missing position is `NO_MATCH`.
27
27
  - **An undeclared path is an error.** A misspelled field is never created; the Kind schema decides
28
- which paths exist. A declared optional field that this revision does not carry stays valid.
28
+ which paths exist. A declared optional field that this revision does not carry stays valid. The one
29
+ relaxation is a migration: `$unset` may address a property the target schema no longer declares,
30
+ because that is exactly what the migration has to remove (see Schema versions).
29
31
  - **`$unset` addresses object properties only.** A collection entry is removed with `$pull`, so an
30
32
  element is never replaced by a hole.
31
33
 
@@ -42,7 +44,9 @@ can pass the first comparison concurrently. A `store` that reports a conflict is
42
44
 
43
45
  A conflict names the current revision and says what to do with it in the error's `repair` field.
44
46
  Only an append at a fixed path can be resent as written: a `$push` whose every path segment is a
45
- plain property adds an entry, and that means the same thing whatever else landed in between.
47
+ plain property adds an entry, and that means the same thing whatever else landed in between — and
48
+ only when the request does not also name `representations`, which were authored against the base
49
+ the caller read, nor a target `schemaVersion`, which was chosen against it.
46
50
  Everything else has to be rebased against a fresh read. `$set` replaces a value the caller has not
47
51
  seen since; `$unset` and `$pull` delete state the caller has not re-read, which the concurrent
48
52
  revision may have written for a reason; a position addresses a different entry once something is
@@ -53,19 +57,55 @@ through a filter can land somewhere the caller never addressed. Field match stil
53
57
  to repeat blindly.
54
58
 
55
59
  A rejected write is a separate case from a refused one. A `store` that returns a conflict persisted
56
- nothing. A `store` that throws leaves the outcome unknown — the contract covers the compare-and-swap,
57
- not what a throw means — so the repair hint asks for a re-read instead of promising a safe retry.
60
+ nothing. A host that refuses before any effect — its own write validator inspecting the payload —
61
+ returns a structured rejection instead of throwing; the tool reports it as `STORE_REJECTED` with the
62
+ host's message and per-path issues, and its repair promises nothing was persisted, so the caller
63
+ corrects the input against the same base revision. A refusal that follows side-effecting steps must
64
+ stay a throw: the safe-resend promise only holds when the whole store attempt left no trace. A `store` that throws leaves the outcome unknown
65
+ — the contract covers the compare-and-swap, not what a throw means — so that repair hint asks for a
66
+ re-read instead of promising a safe retry.
58
67
 
59
68
  Every rejection names the failing path and a repair hint; schema rejections add the expected type or
60
69
  the allowed values per path. `dryRun` applies and validates without persisting.
61
70
 
71
+ ## Schema versions
72
+
73
+ A patch is validated against the registration at the base revision's schema version, so a plain
74
+ patch never moves an artifact between versions. An artifact left behind by a kind bump has no
75
+ registration at its own version any more and is rejected with `SCHEMA_VERSION_MISMATCH`, which
76
+ names the versions that are registered. Naming a target `schemaVersion` on the request is how the
77
+ caller migrates it: the instructions bring the payload to the newer shape, the whole result — declared
78
+ paths and schema alike — is held to the target registration, and the host stores the new revision at
79
+ that version. There is no migration logic in the package; the caller writes the instructions, and
80
+ the target schema's validation of the completed payload is the only judge of the result.
81
+ A migration only moves forward: a target older than the base revision's version is
82
+ `SCHEMA_VERSION_MISMATCH`, whatever is registered. Two rules bend for a migration and only there:
83
+ `$unset` may address a property the target schema does not declare (it must still address something
84
+ the revision carries; `$pull` gets no such relaxation, because pulling from an undeclared collection
85
+ leaves the collection the target refuses), and the patch may carry no instruction at all when the
86
+ stored payload already satisfies the target, so a registration that changed only metadata is
87
+ migrated without a fabricated write. An instructionless patch that targets the version the base
88
+ already has is `NO_CHANGE`.
89
+ A success that moved the artifact reports `migration: { from, to }`; without it, a success must
90
+ list at least one applied instruction.
91
+ A request that names a `schemaVersion` is never resent after a conflict, because the concurrent
92
+ revision may itself have migrated the artifact.
93
+
62
94
  ## Host boundary
63
95
 
64
96
  An integrating product supplies an `ArtifactPatchHost` through `createArtifactPatchToolset(host)` or
65
97
  `createArtifactPatchPackage(host)`. The host owns authorization, repository scope, effective Kind
66
98
  discovery, and persistence. `store` receives the resolved previous revision — payload included — and
67
99
  the request's optional `statusPath`, so a host layered over a lifecycle writer can derive the same
68
- status observation a full revise produces. The package never issues raw Artifact bus requests and
100
+ status observation a full revise produces. It also receives the request's `representations` when
101
+ the caller named them — an object to replace the rendering hints wholesale, `null` to clear them —
102
+ and must carry the previous revision's hints over when the property is absent, because hints are
103
+ caller-authored and the engine cannot tell whether the change made them stale. `store` also receives
104
+ the `schemaVersion` the payload was validated against and must persist the revision at that version,
105
+ which differs from `previous.schemaVersion` exactly when the request migrated the artifact. `store`
106
+ reports its outcome as the persisted revision, a conflict, or a structured rejection for a refusal
107
+ it makes before writing anything; only a failure whose outcome is genuinely unknown should throw.
108
+ The package never issues raw Artifact bus requests and
69
109
  never reaches a store directly, so a service handling `artifact.patch` and the `artifacts_patch` MCP
70
110
  tool run the same engine over the same contract. Its default package marker contributes no tools until a host is
71
111
  explicitly bound.
package/dist/index.d.ts CHANGED
@@ -13,6 +13,6 @@ export default artifactPatchPackage;
13
13
  export { applyArtifactPatch } from './patch-engine.js';
14
14
  export type { ArtifactPatchApplication, ArtifactPatchApplicationResult } from './patch-engine.js';
15
15
  export { executePatchArtifact, patchArtifact } from './patch-artifact.js';
16
- export type { ArtifactPatchHost, ArtifactPatchStoreConflict, ArtifactPatchStoreRequest, ArtifactPatchStoreResult, } from './patch-artifact.js';
16
+ export type { ArtifactPatchHost, ArtifactPatchStoreConflict, ArtifactPatchStoreRejection, ArtifactPatchStoreRequest, ArtifactPatchStoreResult, } from './patch-artifact.js';
17
17
  export { createArtifactPatchToolset, createPatchArtifactTool } from './toolset.js';
18
18
  //# sourceMappingURL=index.d.ts.map
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{a as e,i as t,n,o as r,r as i,s as a,t as o}from"./src-D3gIWmhh.mjs";export{a as applyArtifactPatch,o as artifactPatchPackage,o as default,n as createArtifactPatchPackage,i as createArtifactPatchToolset,t as createPatchArtifactTool,e as executePatchArtifact,r as patchArtifact};
1
+ import{a as e,i as t,n,o as r,r as i,s as a,t as o}from"./src-DQbedDbK.mjs";export{a as applyArtifactPatch,o as artifactPatchPackage,o as default,n as createArtifactPatchPackage,i as createArtifactPatchToolset,t as createPatchArtifactTool,e as executePatchArtifact,r as patchArtifact};
@@ -1,4 +1,4 @@
1
- import { type ArtifactKindRegistration, type ArtifactPatchRequest, type ArtifactPatchResponse, type ArtifactRevision } from '@makaio/framework/contracts';
1
+ import { type ArtifactKindRegistration, type ArtifactPatchIssue, type ArtifactPatchRequest, type ArtifactPatchResponse, type ArtifactRepresentations, type ArtifactRevision } from '@makaio/framework/contracts';
2
2
  import { type ToolExecutionContext, type ToolResult } from '@makaio/framework/tools';
3
3
  /** One new revision the host is asked to persist. */
4
4
  export interface ArtifactPatchStoreRequest {
@@ -13,6 +13,16 @@ export interface ArtifactPatchStoreRequest {
13
13
  readonly previous: ArtifactRevision;
14
14
  /** Patched payload, already validated against the effective kind schema. */
15
15
  readonly data: Record<string, unknown>;
16
+ /**
17
+ * Schema version the new revision is stored at.
18
+ *
19
+ * This is the version of the registration `data` was validated against: the
20
+ * request's target when it named one, else `previous.schemaVersion`. A host
21
+ * must persist the revision at this version rather than copying the previous
22
+ * one, or a migration would be validated against the new shape and stored
23
+ * under the old label.
24
+ */
25
+ readonly schemaVersion: number;
16
26
  /**
17
27
  * Caller-owned status observation for this write, as a `data`-relative JSON
18
28
  * Pointer. Present exactly when the request named one; a host that emits
@@ -20,14 +30,41 @@ export interface ArtifactPatchStoreRequest {
20
30
  * persists, exactly as a full revise does.
21
31
  */
22
32
  readonly statusPath?: string;
33
+ /**
34
+ * Rendering hints for the new revision, present exactly when the request
35
+ * named them. An object replaces the previous revision's hints wholesale and
36
+ * `null` clears them; when the property is absent the host carries
37
+ * `previous.representations` over unchanged, because hints are caller-owned
38
+ * and the patch engine cannot tell whether the change made them stale.
39
+ */
40
+ readonly representations?: ArtifactRepresentations | null;
23
41
  }
24
42
  /** The artifact moved on before the write landed; nothing was persisted. */
25
43
  export interface ArtifactPatchStoreConflict {
26
44
  /** Revision the artifact carries instead of `previous.revision`. */
27
45
  readonly conflictingRevision: string;
28
46
  }
29
- /** Either the persisted revision, or the conflict that stopped it. */
30
- export type ArtifactPatchStoreResult = ArtifactRevision | ArtifactPatchStoreConflict;
47
+ /**
48
+ * The host refused the write before writing anything; nothing was persisted.
49
+ *
50
+ * This is the return shape for a refusal the host makes before any effect — a
51
+ * write validator that inspects the payload and touches nothing. A refusal
52
+ * that surfaces after side-effecting steps ran (a later lifecycle hook
53
+ * rejecting after earlier hooks acted) must stay a throw, because "resend the
54
+ * corrected patch" is only safe when the whole store attempt left no trace.
55
+ * One message plus optional per-path issues, no error taxonomy: the caller's
56
+ * next step is the same for every side-effect-free refusal.
57
+ */
58
+ export interface ArtifactPatchStoreRejection {
59
+ readonly rejection: {
60
+ /** Why the host refused the write. */
61
+ readonly message: string;
62
+ /** Per-path rejections, when the refusal names locations in `data`. */
63
+ readonly issues?: readonly ArtifactPatchIssue[];
64
+ };
65
+ }
66
+ /** The persisted revision, the conflict that stopped it, or the host's refusal. */
67
+ export type ArtifactPatchStoreResult = ArtifactRevision | ArtifactPatchStoreConflict | ArtifactPatchStoreRejection;
31
68
  /**
32
69
  * Host-owned access boundary for patch-based Artifact revisions.
33
70
  *
@@ -65,9 +102,19 @@ export interface ArtifactPatchHost {
65
102
  *
66
103
  * A thrown rejection is reported to the caller as an unknown outcome, because
67
104
  * this contract cannot tell a write that never ran from one that committed
68
- * before the failure surfaced. Refusing a write by returning
69
- * `ArtifactPatchStoreConflict` is the only outcome that promises nothing was
105
+ * before the failure surfaced. A refusal the host makes before any effect —
106
+ * its own write validator inspecting the payload — is returned as
107
+ * `ArtifactPatchStoreRejection` instead of thrown, so the caller learns that
108
+ * nothing was persisted and what to correct. Return it only when the whole
109
+ * store attempt is known to be side-effect-free: a rejection that follows
110
+ * side-effecting steps (a later lifecycle hook refusing after earlier hooks
111
+ * acted) must stay a throw. `ArtifactPatchStoreConflict` remains the refusal
112
+ * for a concurrent revision; both returned refusals promise nothing was
70
113
  * persisted.
114
+ *
115
+ * The new revision is stored at `request.schemaVersion`, which is the version
116
+ * the payload was validated against. It equals `previous.schemaVersion`
117
+ * unless the request migrated the artifact.
71
118
  */
72
119
  store(request: ArtifactPatchStoreRequest, context: ToolExecutionContext): Promise<ArtifactPatchStoreResult>;
73
120
  }
@@ -6,6 +6,16 @@ export interface ArtifactPatchApplication {
6
6
  /** One entry per applied instruction, in application order. */
7
7
  readonly operations: readonly ArtifactPatchOperationResult[];
8
8
  }
9
+ /** Relaxations of the declaration rule that only a migration may ask for. */
10
+ export interface ArtifactPatchApplyOptions {
11
+ /**
12
+ * Let `$unset` remove a property the schema does not declare. Set when the
13
+ * patch migrates to another schema version: a property the target dropped is
14
+ * exactly what the caller has to remove, and the target schema's validation
15
+ * of the completed payload decides whether the result is acceptable.
16
+ */
17
+ readonly allowUndeclaredRemovals?: boolean;
18
+ }
9
19
  /** Outcome of applying a patch document to one payload. */
10
20
  export type ArtifactPatchApplicationResult = {
11
21
  readonly ok: true;
@@ -24,7 +34,8 @@ export type ArtifactPatchApplicationResult = {
24
34
  * @param data - Payload of the revision the patch was written against.
25
35
  * @param patch - Patch document already accepted by its schema.
26
36
  * @param dataSchema - Serialized data schema of the effective kind.
37
+ * @param options - Relaxations that only a migration may ask for.
27
38
  * @returns The patched payload, or the first rejected instruction.
28
39
  */
29
- export declare function applyArtifactPatch(data: Record<string, unknown>, patch: ArtifactPatchDocument, dataSchema: Record<string, unknown>): ArtifactPatchApplicationResult;
40
+ export declare function applyArtifactPatch(data: Record<string, unknown>, patch: ArtifactPatchDocument, dataSchema: Record<string, unknown>, options?: ArtifactPatchApplyOptions): ArtifactPatchApplicationResult;
30
41
  //# sourceMappingURL=patch-engine.d.ts.map
package/dist/server.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { default, applyArtifactPatch, artifactPatchPackage, createArtifactPatchPackage, createArtifactPatchToolset, createPatchArtifactTool, executePatchArtifact, patchArtifact, } from './index.js';
2
- export type { ArtifactPatchApplication, ArtifactPatchApplicationResult, ArtifactPatchHost, ArtifactPatchStoreConflict, ArtifactPatchStoreRequest, ArtifactPatchStoreResult, } from './index.js';
2
+ export type { ArtifactPatchApplication, ArtifactPatchApplicationResult, ArtifactPatchHost, ArtifactPatchStoreConflict, ArtifactPatchStoreRejection, ArtifactPatchStoreRequest, ArtifactPatchStoreResult, } from './index.js';
3
3
  //# sourceMappingURL=server.d.ts.map
package/dist/server.mjs CHANGED
@@ -1 +1 @@
1
- import{a as e,i as t,n,o as r,r as i,s as a,t as o}from"./src-D3gIWmhh.mjs";export{a as applyArtifactPatch,o as artifactPatchPackage,o as default,n as createArtifactPatchPackage,i as createArtifactPatchToolset,t as createPatchArtifactTool,e as executePatchArtifact,r as patchArtifact};
1
+ import{a as e,i as t,n,o as r,r as i,s as a,t as o}from"./src-DQbedDbK.mjs";export{a as applyArtifactPatch,o as artifactPatchPackage,o as default,n as createArtifactPatchPackage,i as createArtifactPatchToolset,t as createPatchArtifactTool,e as executePatchArtifact,r as patchArtifact};
@@ -0,0 +1 @@
1
+ import{ARTIFACT_COLLECTION_ELEMENT_SEGMENT as e,ArtifactPatchIssueSchema as t,ArtifactPatchRequestSchema as n,ArtifactPatchResponseSchema as r,artifactPatchFilterName as i,artifactPatchInstructions as a,artifactPatchSegments as o,compileArtifactDataChecker as s,defineOwnValue as c,inspectArtifactDataLocation as l,isJsonObject as u,jsonEquals as d,ownValue as f,readArtifactTitle as p,readPropertyPath as m}from"@makaio/framework/contracts";import{ToolErrorCodes as h,defineTool as g,defineToolset as _,toolError as v,toolSuccess as ee,widenTool as te}from"@makaio/framework/tools";function y(t){return t.map(t=>t.kind===`property`?t.name:e)}function b(e){return e.length>0&&e.every(e=>typeof e!=`boolean`&&e.type===`array`)}function ne(e){return Object.entries(e).map(([e,t])=>({path:e.split(`.`).slice(1),operand:t}))}function x(e){let t=new Map;for(let n of e.arrayFilters??[]){let e=i(n);e!==void 0&&t.set(e,ne(n))}return t}function S(e,t){return t.every(({path:t,operand:n})=>d(m(e,t),n))}function C(e,t){return u(t)?u(e)?Object.entries(t).every(([t,n])=>d(f(e,t),n)):!1:d(e,t)}function w(e,t,n,r){let i=[];for(let a of e){if(t.kind===`property`){if(!u(a)||r&&!Object.hasOwn(a,t.name))continue;i.push({kind:`property`,container:a,key:t.name});continue}if(!Array.isArray(a))continue;if(t.kind===`index`){t.index<a.length&&i.push({kind:`element`,container:a,index:t.index});continue}let e=n.get(t.placeholder)??[];a.forEach((t,n)=>{S(t,e)&&i.push({kind:`element`,container:a,index:n})})}return i}function T(e){return e.kind===`property`?f(e.container,e.key):e.container[e.index]}function E(e,t){e.kind===`property`?c(e.container,e.key,t):e.container[e.index]=t}function D(e,t,n){let r=[e];for(let e of t){let t=w(r,e,n,!0);if(t.length===0||e.kind!==`filter`&&t.length<r.length)return{ok:!1,segment:e};r=t.map(T)}return{ok:!0,containers:r}}function re(e){return e.kind===`property`?e.name:e.kind===`index`?String(e.index):`$[${e.placeholder}]`}function O(e,t,n,r,i){return{code:e,message:r,operator:t,path:n,repair:i}}function k(e,t,n){return O(`NO_MATCH`,e,t,n,`Addressing nothing is a failure, not a silent no-op: check the match values against the current revision.`)}function ie(e,t){for(let n of e)E(n,structuredClone(t));return e.length}function A(e){for(let t of e)t.kind===`property`&&Reflect.deleteProperty(t.container,t.key);return e.length}function j(e,t){for(let n of e){let e=T(n);if(e===void 0){E(n,[structuredClone(t)]);continue}if(!Array.isArray(e))return;e.push(structuredClone(t))}return e.length}function M(e,t){let n=0;for(let r of e){let e=T(r);if(!Array.isArray(e))return;let i=e.filter(e=>!C(e,t));n+=e.length-i.length,e.splice(0,e.length,...i)}return n}function N(e,t,n){switch(e){case`$set`:return ie(t,n);case`$unset`:return A(t);case`$push`:return j(t,n);case`$pull`:return M(t,n)}}function P(e,t,n){if(t.kind!==`property`){if(e===`$unset`)return O(`UNSUPPORTED_TARGET`,e,n,`$unset addresses object properties; '${n}' addresses a collection entry.`,`Remove a collection entry with $pull instead of $unset.`);if(e===`$push`||e===`$pull`)return O(`UNSUPPORTED_TARGET`,e,n,`${e} addresses a collection; '${n}' addresses one of its entries.`,`Point ${e} at the collection itself, without the trailing entry selector.`)}}function F(e,t,n,r){let i=l(e,y(r));if(i===void 0)return O(`PATH_NOT_DECLARED`,t,n,`The artifact kind does not declare '${n}'.`,`Correct the path to one the kind schema declares; a misspelled field is never created.`);if((t===`$push`||t===`$pull`)&&!b(i))return O(`TARGET_NOT_A_COLLECTION`,t,n,`The artifact kind declares '${n}' as something other than a collection.`,`Use $set to replace '${n}', or point ${t} at a declared collection.`)}function I(e,t,n,r,i){let{operator:a,path:s,value:c}=t,l=o(s),u=l.at(-1);if(u===void 0)return k(a,s,`'${s}' addresses nothing.`);let d=P(a,u,s);if(d)return d;let f=F(r,a,s,l),p=i.allowUndeclaredRemovals===!0&&f?.code===`PATH_NOT_DECLARED`&&a===`$unset`;if(f&&!p)return f;let m=D(e,l.slice(0,-1),n);if(!m.ok){let e=re(m.segment);return m.segment.kind===`property`?O(`PATH_NOT_RESOLVABLE`,a,s,`This revision has no value at '${e}' along '${s}'.`,`Set the value at '${e}' before addressing anything below it.`):k(a,s,`'${e}' in '${s}' addressed no entry in this revision.`)}let h=a===`$unset`||a===`$pull`,g=w(m.containers,u,n,h);if(g.length===0)return k(a,s,`'${s}' addressed no entry in this revision.`);if(u.kind===`index`&&g.length<m.containers.length)return k(a,s,`'${s}' addressed no entry in every selected collection of this revision.`);let _=N(a,g,c);return _===void 0?O(`TARGET_NOT_A_COLLECTION`,a,s,`The value at '${s}' is not a collection in this revision.`,`Use $set to replace '${s}', or point ${a} at a collection.`):_===0?k(a,s,`'${s}' changed nothing in this revision.`):{operator:a,path:s,matched:_}}function L(e,t,n,r={}){let i=structuredClone(e),o=x(t),s=[];for(let e of a(t)){let t=I(i,e,o,n,r);if(`code`in t)return{ok:!1,error:t};s.push(t)}return{ok:!0,application:{data:i,operations:s}}}function R(e){return`conflictingRevision`in e}function z(e){return`rejection`in e}function B(e){let n=(e.rejection.issues??[]).map(e=>t.safeParse(e)).flatMap(e=>e.success?[e.data]:[]),r=e.rejection.message.trim();return{code:`STORE_REJECTED`,message:r===``?`The host refused the write without naming a reason.`:`The host refused the write: ${r}`,...n.length===0?{}:{issues:n},repair:`Nothing was persisted. Correct what the rejection names and resend the patch against the same baseRevision.`}}function V(e){return e instanceof Error?e.message:String(e)}function H(e){return{ok:!1,error:e}}function U(e,t,n){return{refClass:`artifact`,kind:e,id:t,revision:n}}function W(e){return{path:e.path,reason:e.reason,...e.expectedType===void 0?{}:{expectedType:e.expectedType},...e.allowedValues===void 0?{}:{allowedValues:[...e.allowedValues]}}}function G(e){return e.representations!==void 0||e.schemaVersion!==void 0?!1:a(e.patch).every(({operator:e,path:t})=>e===`$push`&&o(t).every(e=>e.kind===`property`))}function K(e,t){return{code:`BASE_REVISION_CONFLICT`,message:`Artifact '${e.ref.kind}:${e.ref.id}' has advanced to revision '${t}'.`,currentRevision:t,repair:G(e)?`Resend the same patch with baseRevision '${t}'; it only appends at a fixed path, with no position and no filter, so it does not depend on the payload you read.`:`Re-read the artifact at revision '${t}' and rewrite the patch: only an append at a fixed path survives a concurrent write, with no position and no filter. Replacing, removing, addressing an entry by position, appending through a $[filter] placeholder, replacing or clearing representations, and naming a target schemaVersion all depend on the payload you read, which the concurrent revision may have changed.`}}function q(e){let t=e[0];if(!t)return`Correct the patched result so it satisfies the kind schema.`;let n=t.path===``?`the payload root`:`'${t.path}'`;return t.allowedValues?`${n} accepts one of: ${t.allowedValues.map(e=>typeof e==`string`?e:JSON.stringify(e)).join(`, `)}.`:t.expectedType?`${n} expects type ${t.expectedType}.`:`${n} ${t.reason}.`}function J(e,t){try{p(e,t.titlePath);return}catch(e){return{code:`SCHEMA_VALIDATION_FAILED`,message:`The patched result does not satisfy the '${t.kind}' data schema.`,issues:[{path:t.titlePath,reason:V(e)}],repair:`'${t.titlePath}' must be a nonblank string.`}}}function ae(e){try{return s(e)}catch(t){return{code:`HOST_FAILED`,message:`Artifact kind '${e.kind}' could not compile its data schema: ${V(t)}`,repair:`Correct the registered data schema before patching artifacts of this kind.`}}}function oe(e,t,n){let r=e.filter(e=>e.kind===t.kind);if(r.length===0)return{code:`KIND_NOT_REGISTERED`,message:`Artifact kind '${t.kind}' is not registered.`,repair:`Register the kind, or address an artifact of a registered kind.`};let i=r.find(e=>e.schemaVersion===n);if(!i){let e=[...new Set(r.map(e=>e.schemaVersion))].sort((e,t)=>e-t),i=e.join(`, `),a=n===t.schemaVersion?`Revision '${t.revision}' uses schema version ${n}`:`The request targets schema version ${n} for revision '${t.revision}' (schema version ${t.schemaVersion})`,o=e.filter(e=>e>t.schemaVersion).join(`, `);return{code:`SCHEMA_VERSION_MISMATCH`,message:`${a}, for which '${t.kind}' has no registration; registered: ${i}.`,repair:o===``?`Register '${t.kind}' at schema version ${n}${n>t.schemaVersion?``:` or newer`}; no registered version is newer than the revision's.`:`Set schemaVersion to one of ${o} and add the instructions that make the payload fit that version, or register '${t.kind}' at schema version ${n}.`}}return i}async function se(e,t,n){let r;try{r=await n.resolveCurrent({kind:e.ref.kind,id:e.ref.id},t)}catch(e){return{code:`HOST_FAILED`,message:`Artifact lookup failed: ${V(e)}`,repair:`Retry once the artifact store is reachable.`}}return r?r.kind!==e.ref.kind||r.id!==e.ref.id?{code:`HOST_FAILED`,message:`Artifact lookup returned a different artifact identity.`,repair:`Retry; the resolved artifact did not match the requested identity.`}:r.revision===e.baseRevision?r:K(e,r.revision):{code:`ARTIFACT_NOT_FOUND`,message:`Artifact '${e.ref.kind}:${e.ref.id}' was not found.`,repair:`Check the kind and identity, or create the artifact before revising it.`}}async function Y(e,t,n){let r=await se(e,t,n);if(`code`in r)return H(r);let i=U(r.kind,r.id,r.revision),a;try{a=await n.listKinds(e.ref.kind,t)}catch(e){return H({code:`HOST_FAILED`,message:`Artifact kind lookup failed: ${V(e)}`,repair:`Retry once the kind catalog is reachable.`})}let o=ce(e,r,a);if(`code`in o)return H(o);let s=o.schemaVersion!==r.schemaVersion,c=ae(o);if(typeof c!=`function`)return H(c);let l=L(r.data,e.patch,o.dataSchema,{allowUndeclaredRemovals:s});if(!l.ok)return H(l.error);let u=c(l.application.data);if(!u.valid)return H({code:`SCHEMA_VALIDATION_FAILED`,message:`The patched result does not satisfy the '${o.kind}' data schema.`,issues:u.issues.map(W),repair:q(u.issues)});let d=J(l.application.data,o);if(d)return H(d);let f=[...l.application.operations],p=s?{migration:{from:r.schemaVersion,to:o.schemaVersion}}:{};if(e.dryRun===!0)return{ok:!0,base:i,dryRun:!0,operations:f,...p};let m;try{m=await n.store({previous:r,data:l.application.data,schemaVersion:o.schemaVersion,...e.statusPath===void 0?{}:{statusPath:e.statusPath},...e.representations===void 0?{}:{representations:e.representations}},t)}catch(e){return H({code:`HOST_FAILED`,message:`Artifact revision failed: ${V(e)}`,repair:`Re-read the artifact: the write may have been committed before the failure was reported. Retry only after confirming the change is absent.`})}if(R(m))return H(K(e,m.conflictingRevision));if(z(m))return H(B(m));let h=le(m,i,o.schemaVersion);return h?H(h):{ok:!0,base:i,dryRun:!1,artifact:U(m.kind,m.id,m.revision),operations:f,...p}}function ce(e,t,n){if(e.schemaVersion!==void 0&&e.schemaVersion<t.schemaVersion)return{code:`SCHEMA_VERSION_MISMATCH`,message:`The request targets schema version ${e.schemaVersion}, older than revision '${t.revision}' (schema version ${t.schemaVersion}); a migration never moves an artifact back.`,repair:`Omit schemaVersion to patch at version ${t.schemaVersion}, or name a newer registered version to migrate the artifact forward.`};let r=oe(n,t,e.schemaVersion??t.schemaVersion);return`code`in r?r:r.schemaVersion===t.schemaVersion&&a(e.patch).length===0?{code:`NO_CHANGE`,message:`The patch carries no instruction and targets schema version ${r.schemaVersion}, which revision '${t.revision}' already has.`,repair:`Add at least one instruction, or name a different schemaVersion to migrate the artifact.`}:r}function le(e,t,n){if(e.kind!==t.kind||e.id!==t.id||e.revision===t.revision)return{code:`HOST_FAILED`,message:`The store returned an artifact that is not a new revision of the patched one.`,repair:`Re-read the artifact before patching it again; the write outcome is unclear.`};if(e.schemaVersion!==n)return{code:`HOST_FAILED`,message:`The store persisted revision '${e.revision}' at schema version ${e.schemaVersion} instead of ${n}.`,repair:`Re-read the artifact; the host did not store the revision at the schema version the patch was validated against.`}}async function X(e,t,n){return n?ee(await Y(e,t,n)):v(h.PERMISSION_DENIED,`Artifact revisions require an authorized host.`)}function Z(e){return g({name:`artifacts_patch`,description:`Revise an Artifact by sending only the change. Name the artifact, the baseRevision you read, and the instructions: $set and $unset on declared paths, $push and $pull on declared collections. Address one collection entry by field match with $[name] plus arrayFilters, or by position. An unknown path, an unknown operator and addressing nothing are all errors, never silent no-ops. Use dryRun to check a patch without writing. A stale baseRevision reports the current revision; follow the repair field of that error: only an append at a fixed path (no position, no filter) may be resent with the new baseRevision, anything else needs a fresh read and a rewritten patch. Rendering hints are separate from data: omit representations to keep them, send an object to replace all of them (no merge), or null to clear them. Name schemaVersion to migrate an artifact left at an older version by a kind bump: the patched result is validated against that version's registration and stored at it; omit it to keep the base revision's version. A migration may $unset a property the target no longer declares, and may carry no instruction when the payload already fits.`,annotations:{readOnly:!1,idempotent:!1},inputSchema:n,outputSchema:r,execute:(t,n)=>X(t,n,e)})}function Q(e){return _({name:`artifact-patch`,description:`Revise Artifacts by patch through an authorized host.`,version:`0.1.0`,tools:[te(Z(e))]})}function $(e){return{name:`artifact-patch`,displayName:`Artifact Patch Tools`,version:`0.1.0`,surface:`headless`,tools:{createToolsets:()=>e?[Q(e)]:[]}}}const ue=$();export{X as a,Z as i,$ as n,Y as o,Q as r,L as s,ue as t};
package/dist/toolset.d.ts CHANGED
@@ -22,7 +22,13 @@ export declare function createPatchArtifactTool(host: ArtifactPatchHost): import
22
22
  arrayFilters: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodType<Record<string, import("@makaio/framework/contracts").JsonValue>, Record<string, import("@makaio/framework/contracts").JsonValue>, import("zod/v4/core").$ZodTypeInternals<Record<string, import("@makaio/framework/contracts").JsonValue>, Record<string, import("@makaio/framework/contracts").JsonValue>>>>>;
23
23
  }, import("zod/v4/core").$strict>;
24
24
  dryRun: import("zod").ZodOptional<import("zod").ZodBoolean>;
25
+ schemaVersion: import("zod").ZodOptional<import("zod").ZodNumber>;
25
26
  statusPath: import("zod").ZodOptional<import("zod").ZodString>;
27
+ representations: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodObject<{
28
+ markdown: import("zod").ZodOptional<import("zod").ZodString>;
29
+ summary: import("zod").ZodOptional<import("zod").ZodString>;
30
+ plaintext: import("zod").ZodOptional<import("zod").ZodString>;
31
+ }, import("zod/v4/core").$strict>>>;
26
32
  }, import("zod/v4/core").$strict>, import("zod").ZodUnion<readonly [import("zod").ZodDiscriminatedUnion<[import("zod").ZodObject<{
27
33
  ok: import("zod").ZodLiteral<true>;
28
34
  base: import("zod").ZodObject<{
@@ -41,6 +47,10 @@ export declare function createPatchArtifactTool(host: ArtifactPatchHost): import
41
47
  path: import("zod").ZodString;
42
48
  matched: import("zod").ZodNumber;
43
49
  }, import("zod/v4/core").$strict>>;
50
+ migration: import("zod").ZodOptional<import("zod").ZodObject<{
51
+ from: import("zod").ZodNumber;
52
+ to: import("zod").ZodNumber;
53
+ }, import("zod/v4/core").$strict>>;
44
54
  dryRun: import("zod").ZodLiteral<true>;
45
55
  }, import("zod/v4/core").$strict>, import("zod").ZodObject<{
46
56
  ok: import("zod").ZodLiteral<true>;
@@ -60,6 +70,10 @@ export declare function createPatchArtifactTool(host: ArtifactPatchHost): import
60
70
  path: import("zod").ZodString;
61
71
  matched: import("zod").ZodNumber;
62
72
  }, import("zod/v4/core").$strict>>;
73
+ migration: import("zod").ZodOptional<import("zod").ZodObject<{
74
+ from: import("zod").ZodNumber;
75
+ to: import("zod").ZodNumber;
76
+ }, import("zod/v4/core").$strict>>;
63
77
  dryRun: import("zod").ZodLiteral<false>;
64
78
  artifact: import("zod").ZodObject<{
65
79
  refClass: import("zod").ZodLiteral<"artifact">;
@@ -75,11 +89,13 @@ export declare function createPatchArtifactTool(host: ArtifactPatchHost): import
75
89
  BASE_REVISION_CONFLICT: "BASE_REVISION_CONFLICT";
76
90
  HOST_FAILED: "HOST_FAILED";
77
91
  KIND_NOT_REGISTERED: "KIND_NOT_REGISTERED";
92
+ NO_CHANGE: "NO_CHANGE";
78
93
  NO_MATCH: "NO_MATCH";
79
94
  PATH_NOT_DECLARED: "PATH_NOT_DECLARED";
80
95
  PATH_NOT_RESOLVABLE: "PATH_NOT_RESOLVABLE";
81
96
  SCHEMA_VALIDATION_FAILED: "SCHEMA_VALIDATION_FAILED";
82
97
  SCHEMA_VERSION_MISMATCH: "SCHEMA_VERSION_MISMATCH";
98
+ STORE_REJECTED: "STORE_REJECTED";
83
99
  TARGET_NOT_A_COLLECTION: "TARGET_NOT_A_COLLECTION";
84
100
  UNSUPPORTED_TARGET: "UNSUPPORTED_TARGET";
85
101
  }>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@makaio/extension-artifact-patch",
3
3
  "description": "Patch-based artifact revision tools for AI agents.",
4
- "version": "1.0.0-dev-1789118938035",
4
+ "version": "1.0.0-dev-1789148673705",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
7
7
  "repository": {
@@ -1 +0,0 @@
1
- import{ARTIFACT_COLLECTION_ELEMENT_SEGMENT as e,ArtifactPatchRequestSchema as t,ArtifactPatchResponseSchema as n,artifactPatchFilterName as r,artifactPatchInstructions as i,artifactPatchSegments as a,compileArtifactDataChecker as o,defineOwnValue as s,inspectArtifactDataLocation as c,isJsonObject as l,jsonEquals as u,ownValue as d,readArtifactTitle as f,readPropertyPath as p}from"@makaio/framework/contracts";import{ToolErrorCodes as m,defineTool as h,defineToolset as g,toolError as ee,toolSuccess as te,widenTool as _}from"@makaio/framework/tools";function v(t){return t.map(t=>t.kind===`property`?t.name:e)}function y(e){return e.length>0&&e.every(e=>typeof e!=`boolean`&&e.type===`array`)}function b(e){return Object.entries(e).map(([e,t])=>({path:e.split(`.`).slice(1),operand:t}))}function x(e){let t=new Map;for(let n of e.arrayFilters??[]){let e=r(n);e!==void 0&&t.set(e,b(n))}return t}function S(e,t){return t.every(({path:t,operand:n})=>u(p(e,t),n))}function C(e,t){return l(t)?l(e)?Object.entries(t).every(([t,n])=>u(d(e,t),n)):!1:u(e,t)}function w(e,t,n,r){let i=[];for(let a of e){if(t.kind===`property`){if(!l(a)||r&&!Object.hasOwn(a,t.name))continue;i.push({kind:`property`,container:a,key:t.name});continue}if(!Array.isArray(a))continue;if(t.kind===`index`){t.index<a.length&&i.push({kind:`element`,container:a,index:t.index});continue}let e=n.get(t.placeholder)??[];a.forEach((t,n)=>{S(t,e)&&i.push({kind:`element`,container:a,index:n})})}return i}function T(e){return e.kind===`property`?d(e.container,e.key):e.container[e.index]}function E(e,t){e.kind===`property`?s(e.container,e.key,t):e.container[e.index]=t}function ne(e,t,n){let r=[e];for(let e of t){let t=w(r,e,n,!0);if(t.length===0||e.kind!==`filter`&&t.length<r.length)return{ok:!1,segment:e};r=t.map(T)}return{ok:!0,containers:r}}function re(e){return e.kind===`property`?e.name:e.kind===`index`?String(e.index):`$[${e.placeholder}]`}function D(e,t,n,r,i){return{code:e,message:r,operator:t,path:n,repair:i}}function O(e,t,n){return D(`NO_MATCH`,e,t,n,`Addressing nothing is a failure, not a silent no-op: check the match values against the current revision.`)}function k(e,t){for(let n of e)E(n,structuredClone(t));return e.length}function A(e){for(let t of e)t.kind===`property`&&Reflect.deleteProperty(t.container,t.key);return e.length}function j(e,t){for(let n of e){let e=T(n);if(e===void 0){E(n,[structuredClone(t)]);continue}if(!Array.isArray(e))return;e.push(structuredClone(t))}return e.length}function M(e,t){let n=0;for(let r of e){let e=T(r);if(!Array.isArray(e))return;let i=e.filter(e=>!C(e,t));n+=e.length-i.length,e.splice(0,e.length,...i)}return n}function N(e,t,n){switch(e){case`$set`:return k(t,n);case`$unset`:return A(t);case`$push`:return j(t,n);case`$pull`:return M(t,n)}}function P(e,t,n){if(t.kind!==`property`){if(e===`$unset`)return D(`UNSUPPORTED_TARGET`,e,n,`$unset addresses object properties; '${n}' addresses a collection entry.`,`Remove a collection entry with $pull instead of $unset.`);if(e===`$push`||e===`$pull`)return D(`UNSUPPORTED_TARGET`,e,n,`${e} addresses a collection; '${n}' addresses one of its entries.`,`Point ${e} at the collection itself, without the trailing entry selector.`)}}function F(e,t,n,r){let i=c(e,v(r));if(i===void 0)return D(`PATH_NOT_DECLARED`,t,n,`The artifact kind does not declare '${n}'.`,`Correct the path to one the kind schema declares; a misspelled field is never created.`);if((t===`$push`||t===`$pull`)&&!y(i))return D(`TARGET_NOT_A_COLLECTION`,t,n,`The artifact kind declares '${n}' as something other than a collection.`,`Use $set to replace '${n}', or point ${t} at a declared collection.`)}function I(e,t,n,r){let{operator:i,path:o,value:s}=t,c=a(o),l=c.at(-1);if(l===void 0)return O(i,o,`'${o}' addresses nothing.`);let u=P(i,l,o);if(u)return u;let d=F(r,i,o,c);if(d)return d;let f=ne(e,c.slice(0,-1),n);if(!f.ok){let e=re(f.segment);return f.segment.kind===`property`?D(`PATH_NOT_RESOLVABLE`,i,o,`This revision has no value at '${e}' along '${o}'.`,`Set the value at '${e}' before addressing anything below it.`):O(i,o,`'${e}' in '${o}' addressed no entry in this revision.`)}let p=i===`$unset`||i===`$pull`,m=w(f.containers,l,n,p);if(m.length===0)return O(i,o,`'${o}' addressed no entry in this revision.`);if(l.kind===`index`&&m.length<f.containers.length)return O(i,o,`'${o}' addressed no entry in every selected collection of this revision.`);let h=N(i,m,s);return h===void 0?D(`TARGET_NOT_A_COLLECTION`,i,o,`The value at '${o}' is not a collection in this revision.`,`Use $set to replace '${o}', or point ${i} at a collection.`):h===0?O(i,o,`'${o}' changed nothing in this revision.`):{operator:i,path:o,matched:h}}function L(e,t,n){let r=structuredClone(e),a=x(t),o=[];for(let e of i(t)){let t=I(r,e,a,n);if(`code`in t)return{ok:!1,error:t};o.push(t)}return{ok:!0,application:{data:r,operations:o}}}function R(e){return`conflictingRevision`in e}function z(e){return e instanceof Error?e.message:String(e)}function B(e){return{ok:!1,error:e}}function V(e,t,n){return{refClass:`artifact`,kind:e,id:t,revision:n}}function H(e){return{path:e.path,reason:e.reason,...e.expectedType===void 0?{}:{expectedType:e.expectedType},...e.allowedValues===void 0?{}:{allowedValues:[...e.allowedValues]}}}function U(e){return i(e.patch).every(({operator:e,path:t})=>e===`$push`&&a(t).every(e=>e.kind===`property`))}function W(e,t){return{code:`BASE_REVISION_CONFLICT`,message:`Artifact '${e.ref.kind}:${e.ref.id}' has advanced to revision '${t}'.`,currentRevision:t,repair:U(e)?`Resend the same patch with baseRevision '${t}'; it only appends at a fixed path, with no position and no filter, so it does not depend on the payload you read.`:`Re-read the artifact at revision '${t}' and rewrite the patch: only an append at a fixed path survives a concurrent write, with no position and no filter. Replacing, removing, addressing an entry by position, and appending through a $[filter] placeholder all depend on the payload you read, which the concurrent revision may have changed.`}}function G(e){let t=e[0];if(!t)return`Correct the patched result so it satisfies the kind schema.`;let n=t.path===``?`the payload root`:`'${t.path}'`;return t.allowedValues?`${n} accepts one of: ${t.allowedValues.map(e=>typeof e==`string`?e:JSON.stringify(e)).join(`, `)}.`:t.expectedType?`${n} expects type ${t.expectedType}.`:`${n} ${t.reason}.`}function K(e,t){try{f(e,t.titlePath);return}catch(e){return{code:`SCHEMA_VALIDATION_FAILED`,message:`The patched result does not satisfy the '${t.kind}' data schema.`,issues:[{path:t.titlePath,reason:z(e)}],repair:`'${t.titlePath}' must be a nonblank string.`}}}function q(e,t){let n=e.filter(e=>e.kind===t.kind);return n.length===0?{code:`KIND_NOT_REGISTERED`,message:`Artifact kind '${t.kind}' is not registered.`,repair:`Register the kind, or address an artifact of a registered kind.`}:n.find(e=>e.schemaVersion===t.schemaVersion)||{code:`SCHEMA_VERSION_MISMATCH`,message:`Revision '${t.revision}' uses schema version ${t.schemaVersion}, for which '${t.kind}' has no registration.`,repair:`Register '${t.kind}' at schema version ${t.schemaVersion}, or migrate the artifact before patching it.`}}async function J(e,t,n){let r;try{r=await n.resolveCurrent({kind:e.ref.kind,id:e.ref.id},t)}catch(e){return{code:`HOST_FAILED`,message:`Artifact lookup failed: ${z(e)}`,repair:`Retry once the artifact store is reachable.`}}return r?r.kind!==e.ref.kind||r.id!==e.ref.id?{code:`HOST_FAILED`,message:`Artifact lookup returned a different artifact identity.`,repair:`Retry; the resolved artifact did not match the requested identity.`}:r.revision===e.baseRevision?r:W(e,r.revision):{code:`ARTIFACT_NOT_FOUND`,message:`Artifact '${e.ref.kind}:${e.ref.id}' was not found.`,repair:`Check the kind and identity, or create the artifact before revising it.`}}async function Y(e,t,n){let r=await J(e,t,n);if(`code`in r)return B(r);let i=V(r.kind,r.id,r.revision),a;try{a=await n.listKinds(e.ref.kind,t)}catch(e){return B({code:`HOST_FAILED`,message:`Artifact kind lookup failed: ${z(e)}`,repair:`Retry once the kind catalog is reachable.`})}let s=q(a,r);if(`code`in s)return B(s);let c=L(r.data,e.patch,s.dataSchema);if(!c.ok)return B(c.error);let l;try{l=o(s)(c.application.data)}catch(e){return B({code:`HOST_FAILED`,message:`Artifact kind '${s.kind}' could not compile its data schema: ${z(e)}`,repair:`Correct the registered data schema before patching artifacts of this kind.`})}if(!l.valid)return B({code:`SCHEMA_VALIDATION_FAILED`,message:`The patched result does not satisfy the '${s.kind}' data schema.`,issues:l.issues.map(H),repair:G(l.issues)});let u=K(c.application.data,s);if(u)return B(u);let d=[...c.application.operations];if(e.dryRun===!0)return{ok:!0,base:i,dryRun:!0,operations:d};let f;try{f=await n.store({previous:r,data:c.application.data,...e.statusPath===void 0?{}:{statusPath:e.statusPath}},t)}catch(e){return B({code:`HOST_FAILED`,message:`Artifact revision failed: ${z(e)}`,repair:`Re-read the artifact: the write may have been committed before the failure was reported. Retry only after confirming the change is absent.`})}return R(f)?B(W(e,f.conflictingRevision)):f.kind!==i.kind||f.id!==i.id||f.revision===i.revision?B({code:`HOST_FAILED`,message:`The store returned an artifact that is not a new revision of the patched one.`,repair:`Re-read the artifact before patching it again; the write outcome is unclear.`}):{ok:!0,base:i,dryRun:!1,artifact:V(f.kind,f.id,f.revision),operations:d}}async function X(e,t,n){return n?te(await Y(e,t,n)):ee(m.PERMISSION_DENIED,`Artifact revisions require an authorized host.`)}function Z(e){return h({name:`artifacts_patch`,description:`Revise an Artifact by sending only the change. Name the artifact, the baseRevision you read, and the instructions: $set and $unset on declared paths, $push and $pull on declared collections. Address one collection entry by field match with $[name] plus arrayFilters, or by position. An unknown path, an unknown operator and addressing nothing are all errors, never silent no-ops. Use dryRun to check a patch without writing. A stale baseRevision reports the current revision; follow the repair field of that error: only an append at a fixed path (no position, no filter) may be resent with the new baseRevision, anything else needs a fresh read and a rewritten patch.`,annotations:{readOnly:!1,idempotent:!1},inputSchema:t,outputSchema:n,execute:(t,n)=>X(t,n,e)})}function Q(e){return g({name:`artifact-patch`,description:`Revise Artifacts by patch through an authorized host.`,version:`0.1.0`,tools:[_(Z(e))]})}function $(e){return{name:`artifact-patch`,displayName:`Artifact Patch Tools`,version:`0.1.0`,surface:`headless`,tools:{createToolsets:()=>e?[Q(e)]:[]}}}const ie=$();export{X as a,Z as i,$ as n,Y as o,Q as r,L as s,ie as t};