@aefree/pi-plastic 0.8.0 → 0.8.1

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 CHANGED
@@ -7,6 +7,12 @@ and this project follows semantic versioning for public package releases.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## [0.8.1] - 2026-10-12
11
+
12
+ ### Fixed
13
+
14
+ - Restored useful bounded direct status text and parseable JSON for standard, machine and XML sources, including exact admitted paths/moves, merge hints, uncertainty and presentation omissions. Native DTOs remain unchanged; opt-in raw diagnostics are separately bounded and cannot restore excluded machine identities.
15
+
10
16
  ## [0.8.0] - 2026-10-08
11
17
 
12
18
  ### Added
@@ -99,7 +99,21 @@ The tested Windows client emitted OEM 437 bytes for some Latin text and replaced
99
99
 
100
100
  Paths: 4,096 characters; revision IDs: 128; status codes: 64; returned rows: 100 default/500 maximum; compact DTO: 131,072 UTF-8 bytes. Unsupported identity-bearing fields are excluded, not clipped. DTO overflow fails closed.
101
101
 
102
- Legacy machine/standard raw presentation (`includeRaw`, `details.rawResult`) and subprocess capture are not bounded by this DTO cap. Omit raw output for normal agent use. Text adds no version lookup; legacy JSON can use a cached `cm version` observation outside DTO provenance.
102
+ ### Registered-tool presentation contract
103
+
104
+ These presentation limits are separate from the unchanged native DTO/domain/resource limits above:
105
+
106
+ | Channel / admitted facts | Limit / overflow policy |
107
+ |---|---|
108
+ | Direct text or parseable JSON, also retained as renderer `details.rawResult` | 32,768 UTF-8 bytes; remove whole trailing rows/lines, disclose presentation returned/omitted counts; never clip an identity |
109
+ | Machine/XML presentation rows | Only validated native rows (100 default/500 maximum); path/source/revision/status bounds unchanged |
110
+ | Standard presentation lines | 100 default/500 maximum, 4,096 code units per whole line; exclude lossy/oversized lines, preserve exact remaining text and disclose omissions |
111
+ | Opt-in raw diagnostic excerpt | 8,192 UTF-8 bytes, whole lines only, source-labelled with privacy warning and omission counts; machine raw withheld if native read/projection is incomplete |
112
+ | Native DTO | Unchanged 131,072 UTF-8 bytes; overflow remains a structured failure |
113
+
114
+ Presentation carries summary, source/read/capture uncertainty, native projection omissions and standard merge-hint counts independently of the DTO. Direct JSON `itemCount` describes its displayed rows; `nativeItemCount` retains the unchanged DTO row accounting, and `presentation` reports additional selection/exclusion. Additional presentation omissions do not change native completeness. Standard lines are external text, not verified reusable path records or conflict/readiness evidence. `includeRaw` never bypasses identity exclusions; omit it for normal agent use. Registered JSON presentation adds no version lookup. The exported legacy `status` operation keeps its existing presentation interface.
115
+
116
+ Outcome mapping is unchanged: successful partial machine reads remain `ok:true`; standard read is unknown; strict XML rejects unsupported/unsafe source data; operation/capture/DTO failures return `isError:true` and bounded `ok:false`. Presentation does not rerun commands, change effects, or authorize replay.
103
117
 
104
118
  ## Standard
105
119
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aefree/pi-plastic",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22.19.0"
@@ -1,6 +1,6 @@
1
1
  import { Type, type Static, type TSchema } from "typebox";
2
2
  import { Check } from "typebox/value";
3
- import { assembleStatusObservation, presentStatusObservation, type StatusObservationOptions, type StatusObservation, StatusOptionError, StatusXmlError } from "../operations/status";
3
+ import { assembleStatusObservation, type StatusObservationOptions, type StatusObservation, StatusOptionError, StatusXmlError } from "../operations/status";
4
4
  import { StatusCommandError } from "../execution/status-command";
5
5
 
6
6
  export const STATUS_OUTPUT_MAX_BYTES = 131072;
@@ -130,19 +130,74 @@ export function projectStatusOutput(observation: StatusObservation, maxItems = 1
130
130
  });
131
131
  }
132
132
 
133
+ // Presentation ceilings, independent of native DTO and transport/domain limits.
134
+ export const STATUS_PRESENTATION_MAX_BYTES = 32768;
135
+ export const STATUS_RAW_PRESENTATION_MAX_BYTES = 8192;
136
+ const safeLine = (line: string) => !line.includes("�") && !(process.platform === "win32" && line.includes("?")) && !/[\u0000-\u0008\u000b\u000c\u000e-\u001e]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/.test(line);
137
+ const lines = (text: string) => text.split(/\r?\n/).filter(line => line.trim().length > 0);
138
+
139
+ /** Render native admitted rows, not legacy raw presentation. No command/version lookup. */
140
+ function presentStatusOutput(observation: StatusObservation, dto: Extract<StatusOutput, { ok: true }>, args: { format?: "text" | "json"; maxItems?: number; includeRaw?: boolean }): string {
141
+ const standardLines = observation.kind === "standard" ? lines(observation.output) : [];
142
+ const admittedLines = standardLines.filter(line => line.length <= 4096 && safeLine(line));
143
+ const selectedLines = admittedLines.slice(0, args.maxItems ?? 100);
144
+ const selectedItems = dto.data.mode === "standard" ? [] : [...dto.data.items];
145
+ const rawSource = observation.kind === "xml" ? "status_xml (utf-8)" : observation.kind === "machine" ? "status --machinereadable" : "status text";
146
+ const rawWarning = observation.kind === "xml" ? "Raw XML contains private workspace/repository/server metadata." : "Raw status contains private workspace/repository/server metadata.";
147
+ // A diagnostic must not resurrect machine identities withheld by native admission.
148
+ const rawWithheld = observation.kind === "machine" && (dto.completeness.read !== "complete" || !dto.completeness.projection);
149
+ const rawLines = args.includeRaw && !rawWithheld ? lines(observation.output) : [];
150
+ const selectedRaw: string[] = [];
151
+ let rawBytes = 0;
152
+ for (const line of rawLines) {
153
+ const bytes = Buffer.byteLength(line, "utf8") + (selectedRaw.length ? 1 : 0);
154
+ if (!safeLine(line) || (observation.kind === "standard" && line.length > 4096) || rawBytes + bytes > STATUS_RAW_PRESENTATION_MAX_BYTES) continue;
155
+ selectedRaw.push(line); rawBytes += bytes;
156
+ }
157
+ const render = () => {
158
+ const presentation = dto.data.mode === "standard"
159
+ ? { basis: "external_text_lines", total: standardLines.length, returned: selectedLines.length, omitted: standardLines.length - selectedLines.length, excluded: standardLines.length - admittedLines.length }
160
+ : { basis: "native_admitted_rows", total: dto.data.itemCount.parsed, returned: selectedItems.length, omitted: dto.data.itemCount.parsed - selectedItems.length, excluded: dto.data.itemCount.excluded };
161
+ const raw = args.includeRaw ? { source: rawSource, warning: rawWarning, withheld: rawWithheld, text: selectedRaw.join("\n"), returnedLines: selectedRaw.length, omittedLines: rawWithheld ? null : rawLines.length - selectedRaw.length } : undefined;
162
+ const data = dto.data.mode === "standard"
163
+ ? { ...dto.data, lines: selectedLines, presentation }
164
+ : { ...dto.data, items: selectedItems, nativeItemCount: dto.data.itemCount, itemCount: { parsed: presentation.total, returned: presentation.returned, omitted: presentation.omitted, excluded: presentation.excluded }, presentation };
165
+ if (args.format === "json") return JSON.stringify({ ok: true, action: "status", completeness: dto.completeness, data, ...(raw ? { rawOutput: raw } : {}) });
166
+ const heading = dto.data.mode === "standard" ? `Status (standard text): ${dto.data.summary.totalPending} classified short-output lines.` : `Status ${dto.data.mode} (synthesized): ${dto.data.itemCount.parsed} parsed records.`;
167
+ const rows = dto.data.mode === "standard" ? selectedLines : selectedItems.map(row => `${row.statusCode} ${row.sourcePath ? `${row.sourcePath} -> ` : ""}${row.path}${row.isDirectory ? " [directory]" : ""}${"revisionId" in row ? ` [revision ${row.revisionId}]` : ""}`);
168
+ const hints = dto.data.mode === "standard" ? [`Merge hints (text_hints): ${dto.data.mergeHints.pendingMergeLinkCount} pending merge links; ${dto.data.mergeHints.mergeInProgressHintCount} merge-in-progress hints (not verified conflicts).`] : [];
169
+ return [heading, `Read: ${dto.completeness.read}; capture: ${dto.completeness.capture}; native projection: ${dto.completeness.projection ? "complete" : "incomplete"}.`, `Summary: ${JSON.stringify(dto.data.summary)}`, ...hints, ...rows,
170
+ `Presentation: ${presentation.returned} returned; ${presentation.omitted} omitted; ${presentation.excluded} excluded.`,
171
+ ...(dto.data.mode !== "standard" ? [`Native rows: ${dto.data.itemCount.returned} returned; ${dto.data.itemCount.omitted} omitted; ${dto.data.itemCount.excluded} excluded.`] : []),
172
+ ...(dto.data.mode === "xml" ? ["Workspace scope; absolute paths; base revisions unavailable."] : []),
173
+ ...(raw ? [raw.warning, `Raw ${raw.source}: ${raw.withheld ? "withheld due to incomplete native admission" : `${raw.returnedLines} lines returned; ${raw.omittedLines} omitted`}`, raw.text] : []),
174
+ ].join("\n");
175
+ };
176
+ let result = render();
177
+ while (Buffer.byteLength(result, "utf8") > STATUS_PRESENTATION_MAX_BYTES) {
178
+ // Drop whole records only; JSON is serialized after selection, never byte-sliced.
179
+ if (selectedRaw.length) selectedRaw.pop();
180
+ else if (selectedItems.length) selectedItems.pop();
181
+ else if (selectedLines.length) selectedLines.pop();
182
+ else throw new Error("Status fixed presentation exceeded its ceiling.");
183
+ result = render();
184
+ }
185
+ return result;
186
+ }
187
+
133
188
  export async function executeStatusOutput(args: StatusObservationOptions & { format?: "text" | "json"; maxItems?: number; includeRaw?: boolean }) {
134
189
  let dto: StatusOutput;
135
190
  let rawResult: string | undefined;
136
191
  try {
137
192
  const observation = await assembleStatusObservation(args);
138
193
  dto = projectStatusOutput(observation, args.maxItems);
139
- if (dto.ok) rawResult = await presentStatusObservation(observation, args);
194
+ if (dto.ok) rawResult = presentStatusOutput(observation, dto, args);
140
195
  } catch (error) {
141
196
  const version = args.source === "xml" ? 2 : 1;
142
197
  dto = validateStatusOutput(error instanceof StatusXmlError ? ({ ...envelope, schemaVersion: 2, ok: false, completeness: { read: "incomplete", capture: "complete", projection: false }, error: { code: error.code, message: error.message } }) : error instanceof StatusCommandError ? errorOutput(error.code, error.message, version) : error instanceof StatusOptionError ? errorOutput("command_failed", error.message, version) : errorOutput("invalid_producer_data", "Status producer failed to produce valid data.", version), version);
143
198
  }
144
199
  return {
145
- content: [{ type: "text" as const, text: dto.ok ? dto.data.mode !== "standard" ? `Status: ${dto.data.itemCount.parsed} parsed records; ${dto.data.itemCount.returned} returned. Read: ${dto.completeness.read}.` : `Status: ${dto.data.summary.totalPending} classified short-output lines. Read: unknown.` : dto.error.message }],
200
+ content: [{ type: "text" as const, text: dto.ok ? rawResult! : args.format === "json" ? JSON.stringify(dto) : dto.error.message }],
146
201
  structuredContent: dto, isError: !dto.ok,
147
202
  details: { exportName: "status", rawResult, ...(args.workdir && (dto.ok || args.source !== "xml") ? { workdir: args.workdir } : {}) },
148
203
  };