@punica/editor 1.24.0 → 1.25.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@punica/editor",
3
- "version": "1.24.0",
3
+ "version": "1.25.0",
4
4
  "description": "Punica Editor",
5
5
  "private": false,
6
6
  "type": "module",
@@ -26,6 +26,7 @@
26
26
  "check:permissions": "node scripts/check-permissions.mjs",
27
27
  "check:empty-states": "node scripts/check-empty-states.mjs",
28
28
  "check:capability-handlers": "node scripts/check-capability-handlers.mjs",
29
+ "report:capability-actions": "node scripts/report-capability-actions.mjs",
29
30
  "test": "vitest run",
30
31
  "test:watch": "vitest",
31
32
  "test:ui": "vitest --ui",
@@ -176,6 +176,45 @@ declare module 'punica' {
176
176
  [key: string]: unknown;
177
177
  }
178
178
 
179
+ /**
180
+ * What a capability DOES, from a closed vocabulary — the axis a policy
181
+ * rule can address when the id space cannot.
182
+ *
183
+ * The substrate ships two ids for the same action more than once:
184
+ * `fs.readFile` and `workspace.readFile` read the same file, so a rule
185
+ * denying `fs.*` says nothing about the second one. Measured
186
+ * 2026-08-30 with a model in the loop and again without one: same
187
+ * workspace, same path, identical `argsBinding`, one denied and one
188
+ * allowed. The rule engine was right; the vocabulary the rules address
189
+ * was too narrow. A rule may now select `action: [file.read]` and
190
+ * cover both.
191
+ *
192
+ * Closed on purpose, unlike the open `sideEffects` list beside it. Two
193
+ * reasons: an open vocabulary cannot be relied on by a rule author who
194
+ * has to guess the spelling, and `extension-policy-export` expands an
195
+ * action rule back into ids before it leaves the machine — an
196
+ * expansion is only sound while both sides agree on the term.
197
+ *
198
+ * Declaring is voluntary and its absence changes nothing: a capability
199
+ * with no `action` is matched by id exactly as it was before this
200
+ * existed. There is no implicit deny anywhere in this file.
201
+ */
202
+ export type CapabilityAction =
203
+ | 'file.read'
204
+ | 'file.write'
205
+ | 'file.list'
206
+ | 'file.stat'
207
+ | 'secret.read'
208
+ | 'secret.write'
209
+ | 'process.exec'
210
+ | 'net.request'
211
+ | 'model.invoke'
212
+ | 'memory.read'
213
+ | 'memory.write'
214
+ | 'vcs.read'
215
+ | 'vcs.write'
216
+ | 'flow.run';
217
+
179
218
  /**
180
219
  * Unified Capability Definition - Base interface for all capability types.
181
220
  *
@@ -267,6 +306,21 @@ declare module 'punica' {
267
306
  */
268
307
  sideEffects?: string[];
269
308
 
309
+ /**
310
+ * What this capability does, from the closed `CapabilityAction`
311
+ * vocabulary — the axis `.punica/policy.yaml` rules can address
312
+ * instead of an id.
313
+ *
314
+ * A list rather than one value because a capability can genuinely do
315
+ * two things: `fs.copy` reads a file and writes another, and
316
+ * declaring only the write would hide half of what a rule needs to
317
+ * see. Declare what the call PERFORMS, not what its name suggests —
318
+ * `search.query` returns matching lines, so it reads files.
319
+ *
320
+ * Contract: `docs/capability-actions.md`.
321
+ */
322
+ action?: CapabilityAction[];
323
+
270
324
  /**
271
325
  * Required context for this capability to execute.
272
326
  * Examples: ['workspace', 'active-file', 'selection']
@@ -132,6 +132,20 @@ declare module 'punica' {
132
132
  usage?: runtime.LlmUsage;
133
133
  /** Wall-clock duration of this turn's LLM call, in milliseconds. */
134
134
  latencyMs?: number;
135
+ /**
136
+ * Why the turn ended without a model response. Absent on a turn
137
+ * that produced one, which is every turn in a run that finished
138
+ * normally.
139
+ *
140
+ * The field exists because the turn's span has to be published
141
+ * either way: the model call and any compaction before it are
142
+ * already children of it, and a parent nobody emits leaves the
143
+ * reader a tree of ghosts. So a failed turn is a node that says
144
+ * what happened to it rather than a node that is missing.
145
+ */
146
+ outcome?: 'error' | 'cancelled';
147
+ /** The failure text, when `outcome` is `'error'`. */
148
+ error?: string;
135
149
  }
136
150
 
137
151
  /**
@@ -138,6 +138,18 @@ declare module 'punica' {
138
138
  modelId?: string;
139
139
  provider?: string;
140
140
  cacheHit?: boolean;
141
+ /**
142
+ * Content digest of the prompt that produced this answer —
143
+ * see `runtime.LlmCallMeta.promptVersion`.
144
+ *
145
+ * It rides on the OUTPUT rather than only on the `llm.call`
146
+ * event because that event is published `kind: 'info'` and
147
+ * dies with the in-memory ring, while a capability's output
148
+ * reaches the durable audit record. An evidence package
149
+ * answering "which prompt produced this run" has to read it
150
+ * from disk.
151
+ */
152
+ promptVersion?: string;
141
153
  };
142
154
  }
143
155
 
@@ -16,6 +16,20 @@ declare module 'punica' {
16
16
  * Identifier within the kind (e.g. capabilityId, providerId).
17
17
  */
18
18
  id: string;
19
+ /**
20
+ * What the guarded action performs, from the closed
21
+ * `CapabilityAction` vocabulary — read off the capability's own
22
+ * declaration by the gate, never invented here.
23
+ *
24
+ * This is what lets a rule cover a synonym: `fs.readFile` and
25
+ * `workspace.readFile` are two ids for one action, and before
26
+ * this field a rule about the first was silent about the second.
27
+ *
28
+ * Absent for a request with no capability behind it (`llm.remote`,
29
+ * a flow step) and for a capability that declares nothing — both
30
+ * key exactly as they did before actions existed.
31
+ */
32
+ action?: CapabilityAction[];
19
33
  risk: PolicyRisk;
20
34
  approval: PolicyApproval;
21
35
  /**
@@ -376,6 +390,27 @@ declare module 'punica' {
376
390
  kind: string;
377
391
  /** Exact id, or a trailing-`*` prefix glob. Absent matches every id of the kind. */
378
392
  id?: string;
393
+ /**
394
+ * Which actions this rule is about, from the closed
395
+ * `CapabilityAction` vocabulary — the selector that covers a
396
+ * synonym the id space hides. A rule denying `fs.*` said nothing
397
+ * about `workspace.readFile`, which reads the same file
398
+ * (measured 2026-08-30); `action: ['file.read']` covers both.
399
+ *
400
+ * ANDed with `id` when both are present. Absent matches on id
401
+ * alone, which is every rule written before this existed.
402
+ *
403
+ * The quantifier depends on the effect, for the same reason a
404
+ * wildcard condition's does. For `deny`/`require`, ANY declared
405
+ * action named here is enough. For `allow`, EVERY action the
406
+ * capability declares must be named — otherwise an allow written
407
+ * for `file.write` would also release `fs.copy`, which reads.
408
+ *
409
+ * A capability that declares no action is matched by id only; an
410
+ * action rule never reaches it. Contract:
411
+ * `docs/capability-actions.md`.
412
+ */
413
+ action?: CapabilityAction[];
379
414
  effect: PolicyRuleEffect;
380
415
  /** ANDed. Absent or empty matches every argument tuple. */
381
416
  when?: PolicyCondition[];
@@ -395,6 +430,12 @@ declare module 'punica' {
395
430
  reason?: string;
396
431
  /** Digest of the template's rules — which policy text was in force. */
397
432
  digest?: string;
433
+ /**
434
+ * Which selector matched: the capability's id, or an action it
435
+ * declares. A reader of the audit record can otherwise not tell
436
+ * why a rule naming no id applied to this call.
437
+ */
438
+ matchedBy?: 'id' | 'action';
398
439
  }
399
440
 
400
441
  /** A rule that matched, plus the rule itself for the caller to read. */
@@ -252,6 +252,29 @@ declare module 'punica' {
252
252
  * "gpt-4o", "llama3:70b-q4_K_M").
253
253
  */
254
254
  model: string;
255
+ /**
256
+ * Which prompt was used, as a content digest of the resolved
257
+ * instruction class — its id, system prompt, output schema and
258
+ * response format, SHA-256 truncated to 128 bits of hex.
259
+ *
260
+ * A digest rather than a version number because there is no
261
+ * version to read: no `.ic.yaml` carries one and
262
+ * `InstructionClass` has no such field, so any counter here would
263
+ * be a number nobody maintains. This is the field LLMOps 2.1 left
264
+ * open, and the decision it was waiting for.
265
+ *
266
+ * Deliberately NOT covered: `temperature`, `maxTokens`,
267
+ * `expertModelId` and the provider chain. Those are how the call
268
+ * was made rather than what was asked, and the model that
269
+ * answered is already on `model` beside this. Two calls sharing a
270
+ * `promptVersion` were asked the same thing — not necessarily
271
+ * under the same conditions.
272
+ *
273
+ * Absent on a path that resolves no instruction class
274
+ * (`llm.chat`, `llm.chatStream`), which is every call the model
275
+ * makes directly rather than through a named prompt.
276
+ */
277
+ promptVersion?: string;
255
278
  promptTokens: number;
256
279
  completionTokens: number;
257
280
  /**