@danypops/papyrus 0.45.1 → 0.45.2

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": "@danypops/papyrus",
3
- "version": "0.45.1",
3
+ "version": "0.45.2",
4
4
  "description": "Daemon-backed graph artifacts, evidence-bearing tasks, rules, skills, and native TUI workflows for Pi",
5
5
  "type": "module",
6
6
  "keywords": ["pi-package"],
@@ -124,8 +124,12 @@ const showCommand = buildCommand({
124
124
 
125
125
  const previewCommand = buildCommand({
126
126
  func: async function (this: RulesContext, _flags: Record<string, never>, id: string) {
127
- const preview = await this.client.call<Record<string, unknown>, string>("rules.preview", { id });
128
- render.call(this, preview, preview);
127
+ const result = await this.client.call<Record<string, unknown>, { preview: string; combinedLength: number; warning?: string }>(
128
+ "rules.preview",
129
+ { id },
130
+ );
131
+ const text = result.warning === undefined ? result.preview : `${result.preview}\n\n⚠ ${result.warning}`;
132
+ render.call(this, result, text);
129
133
  },
130
134
  parameters: { flags: {}, positional: { kind: "tuple", parameters: [{ brief: "Rule id", parse: String, placeholder: "id" }] } },
131
135
  docs: { brief: "Render a Rule's own condition/action/body preview text" },
@@ -16,6 +16,7 @@ import {
16
16
  PLAYBOOK_INVOCATION_MAX_LINKED_ARTIFACTS,
17
17
  PLAYBOOK_MAX_STEPS,
18
18
  RULE_TEXT_HARD_LIMIT_CHARACTERS,
19
+ RULE_TEXT_SOFT_TARGET_CHARACTERS,
19
20
  SKILL_MAX_ENUM_VALUES,
20
21
  } from "./constants.ts";
21
22
  import {
@@ -312,8 +313,26 @@ export type RuleTransition = "enable" | "disable";
312
313
  * reviewed, and a warning nobody reads is not a bound. See RULE_TEXT_HARD_LIMIT_CHARACTERS's
313
314
  * own comment in constants.ts for the research this threshold is grounded in.
314
315
  */
316
+ export function ruleCombinedLength(condition: string | undefined, action: string | undefined, body: string | undefined): number {
317
+ return (condition ?? "").length + (action ?? "").length + (body ?? "").length;
318
+ }
319
+
320
+ /**
321
+ * Non-blocking counterpart to assertRuleTextWithinBounds's hard rejection: the same combined
322
+ * length, informational once it crosses the soft target, so a caller doesn't have to self-police
323
+ * with a manual character count before every rules.create/update. Returns undefined at or under
324
+ * the target -- the common case, not worth a field only ever seen as "undefined" on the wire.
325
+ */
326
+ export function ruleCombinedLengthWarning(combinedLength: number): string | undefined {
327
+ if (combinedLength <= RULE_TEXT_SOFT_TARGET_CHARACTERS) return undefined;
328
+ return (
329
+ `condition+action+body is ${combinedLength} characters, over the ${RULE_TEXT_SOFT_TARGET_CHARACTERS}-character soft target ` +
330
+ `(hard limit ${RULE_TEXT_HARD_LIMIT_CHARACTERS}) -- consider moving detail into a linked Doc.`
331
+ );
332
+ }
333
+
315
334
  function assertRuleTextWithinBounds(condition: string | undefined, action: string | undefined, body: string | undefined): void {
316
- const combined = (condition ?? "").length + (action ?? "").length + (body ?? "").length;
335
+ const combined = ruleCombinedLength(condition, action, body);
317
336
  if (combined > RULE_TEXT_HARD_LIMIT_CHARACTERS) {
318
337
  throw new Error(
319
338
  `rule condition+action+body is ${combined} characters, exceeding the ${RULE_TEXT_HARD_LIMIT_CHARACTERS}-character bound. ` +
@@ -49,7 +49,7 @@ export function registerRulesVehicleOperations(registry: VehicleRegistry, artifa
49
49
 
50
50
  define(
51
51
  "create",
52
- "Creates a Rule -- a standing constraint injected into the agent system prompt while active. project_root is optional (omitted = unscoped).",
52
+ "Creates a Rule -- a standing constraint injected into the agent system prompt while active. project_root is optional (omitted = unscoped). The response includes combinedLength (condition+action+body character count) and a non-blocking warning once it exceeds the ~600-character soft target (hard-rejected past 4000).",
53
53
  "local-write",
54
54
  {
55
55
  title: stringProp,
@@ -77,14 +77,21 @@ export function registerRulesVehicleOperations(registry: VehicleRegistry, artifa
77
77
  (input) => input,
78
78
  );
79
79
 
80
- define("show", "Shows one Rule by id or title.", "read", { id: stringProp, name: stringProp, project_root: stringProp }, [], (input) => ({
81
- ...input,
82
- id: resolveRuleId(artifacts, scopes, input.project_root as string | undefined, input.id, input.name),
83
- }));
80
+ define(
81
+ "show",
82
+ "Shows one Rule by id or title. The response includes combinedLength (condition+action+body character count) and a non-blocking warning once it exceeds the ~600-character soft target.",
83
+ "read",
84
+ { id: stringProp, name: stringProp, project_root: stringProp },
85
+ [],
86
+ (input) => ({
87
+ ...input,
88
+ id: resolveRuleId(artifacts, scopes, input.project_root as string | undefined, input.id, input.name),
89
+ }),
90
+ );
84
91
 
85
92
  define(
86
93
  "preview",
87
- "Renders a Rule's own condition/action/body preview text with no side effects.",
94
+ "Renders a Rule's own condition/action/body preview text with no side effects. Response: { preview, combinedLength, warning? } -- warning is present only once combinedLength exceeds the ~600-character soft target.",
88
95
  "read",
89
96
  { id: stringProp, name: stringProp, project_root: stringProp },
90
97
  [],
@@ -146,7 +153,7 @@ export function registerRulesVehicleOperations(registry: VehicleRegistry, artifa
146
153
 
147
154
  define(
148
155
  "update",
149
- "Changes a Rule's title/body/labels (at least one required). Body updates still enforce the same combined condition+action+body context-tax bound as creation.",
156
+ "Changes a Rule's title/body/labels (at least one required). Body updates still enforce the same combined condition+action+body context-tax bound as creation. The response includes combinedLength and a non-blocking warning once it exceeds the ~600-character soft target.",
150
157
  "local-write",
151
158
  {
152
159
  id: stringProp,
@@ -11,7 +11,7 @@
11
11
  * registry" convention.
12
12
  */
13
13
 
14
- import { summarizeArtifact } from "../artifact/artifact.ts";
14
+ import { type Artifact, summarizeArtifact } from "../artifact/artifact.ts";
15
15
  import type { ArtifactScopeStore } from "../artifact/artifact-scope-store.ts";
16
16
  import type { ArtifactStore } from "../artifact/artifact-store.ts";
17
17
  import {
@@ -20,6 +20,8 @@ import {
20
20
  gateTaskWithRule,
21
21
  listRules,
22
22
  previewRule,
23
+ ruleCombinedLength,
24
+ ruleCombinedLengthWarning,
23
25
  showRule,
24
26
  transitionRule,
25
27
  updateRule,
@@ -42,6 +44,22 @@ const artifactFilter = (input: OperationInput) => ({
42
44
  projectRoot: optionalString(input, "project_root"),
43
45
  });
44
46
 
47
+ /**
48
+ * Adds a rule's own combinedLength (and, past the soft target, a non-blocking warning) to any
49
+ * response shaped as its own Artifact -- additive fields alongside every existing one, so a
50
+ * caller reading .id/.title/... unchanged still works. Removes the need for a manual
51
+ * len(condition)+len(action)+len(body) count before every rules.create/update call.
52
+ */
53
+ function withRuleLengthInfo(rule: Artifact): Artifact & { combinedLength: number; warning?: string } {
54
+ const combinedLength = ruleCombinedLength(
55
+ typeof rule.extra.condition === "string" ? rule.extra.condition : undefined,
56
+ typeof rule.extra.action === "string" ? rule.extra.action : undefined,
57
+ rule.body,
58
+ );
59
+ const warning = ruleCombinedLengthWarning(combinedLength);
60
+ return { ...rule, combinedLength, ...(warning === undefined ? {} : { warning }) };
61
+ }
62
+
45
63
  /** Registers every rules.* operation except rules.injectable (see module comment). Behavior is unchanged from the prior inline handlers in src/service.ts. */
46
64
  /** This module's own operation names, the single source of truth src/service.ts's EXPECTED_OPERATION_NAMES spreads in rather than re-listing by hand. rules.injectable is deliberately absent -- see the module comment above. */
47
65
  export const RULES_OPERATION_NAMES = [
@@ -64,28 +82,39 @@ export function rulesOperations(artifacts: ArtifactStore, scopes: ArtifactScopeS
64
82
  });
65
83
  return [
66
84
  define("rules.create", (input: OperationInput) =>
67
- createRule(
68
- artifacts,
69
- scopes,
70
- {
71
- title: string(input, "title"),
72
- body: optionalString(input, "body"),
73
- condition: optionalString(input, "condition"),
74
- action: optionalString(input, "rule_action") ?? optionalString(input, "governance_action"),
75
- severity: optionalString(input, "severity") as "block" | "warn" | "info" | undefined,
76
- labels: input.labels as string[] | undefined,
77
- extra: input.extra as Record<string, unknown> | undefined,
78
- projectRoot: optionalString(input, "project_root"),
79
- },
80
- eventContext(input),
85
+ withRuleLengthInfo(
86
+ createRule(
87
+ artifacts,
88
+ scopes,
89
+ {
90
+ title: string(input, "title"),
91
+ body: optionalString(input, "body"),
92
+ condition: optionalString(input, "condition"),
93
+ action: optionalString(input, "rule_action") ?? optionalString(input, "governance_action"),
94
+ severity: optionalString(input, "severity") as "block" | "warn" | "info" | undefined,
95
+ labels: input.labels as string[] | undefined,
96
+ extra: input.extra as Record<string, unknown> | undefined,
97
+ projectRoot: optionalString(input, "project_root"),
98
+ },
99
+ eventContext(input),
100
+ ),
81
101
  ),
82
102
  ),
83
103
  define("rules.list", (input: OperationInput) => {
84
104
  const rules = listRules(artifacts, scopes, artifactFilter(input));
85
105
  return optionalBoolean(input, "full") === true ? rules : rules.map(summarizeArtifact);
86
106
  }),
87
- define("rules.show", (input: OperationInput) => showRule(artifacts, string(input, "id"))),
88
- define("rules.preview", (input: OperationInput) => previewRule(artifacts, string(input, "id"))),
107
+ define("rules.show", (input: OperationInput) => withRuleLengthInfo(showRule(artifacts, string(input, "id")))),
108
+ define("rules.preview", (input: OperationInput) => {
109
+ const rule = showRule(artifacts, string(input, "id"));
110
+ const combinedLength = ruleCombinedLength(
111
+ typeof rule.extra.condition === "string" ? rule.extra.condition : undefined,
112
+ typeof rule.extra.action === "string" ? rule.extra.action : undefined,
113
+ rule.body,
114
+ );
115
+ const warning = ruleCombinedLengthWarning(combinedLength);
116
+ return { preview: previewRule(artifacts, string(input, "id")), combinedLength, ...(warning === undefined ? {} : { warning }) };
117
+ }),
89
118
  define("rules.enable", (input: OperationInput) => transitionRule(artifacts, string(input, "id"), "enable", eventContext(input))),
90
119
  define("rules.disable", (input: OperationInput) => transitionRule(artifacts, string(input, "id"), "disable", eventContext(input))),
91
120
  define("rules.gate", (input: OperationInput) =>
@@ -95,15 +124,17 @@ export function rulesOperations(artifacts: ArtifactStore, scopes: ArtifactScopeS
95
124
  assignRuleProject(artifacts, scopes, string(input, "id"), optionalString(input, "project_root")),
96
125
  ),
97
126
  define("rules.update", (input: OperationInput) =>
98
- updateRule(
99
- artifacts,
100
- string(input, "id"),
101
- {
102
- title: optionalString(input, "title"),
103
- body: optionalString(input, "body"),
104
- labels: input.labels as string[] | undefined,
105
- },
106
- eventContext(input),
127
+ withRuleLengthInfo(
128
+ updateRule(
129
+ artifacts,
130
+ string(input, "id"),
131
+ {
132
+ title: optionalString(input, "title"),
133
+ body: optionalString(input, "body"),
134
+ labels: input.labels as string[] | undefined,
135
+ },
136
+ eventContext(input),
137
+ ),
107
138
  ),
108
139
  ),
109
140
  ];