@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 +6 -0
- package/docs/2026-10-06-status.md +15 -1
- package/package.json +1 -1
- package/src/pi/status-output.ts +58 -3
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
|
-
|
|
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
package/src/pi/status-output.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Type, type Static, type TSchema } from "typebox";
|
|
2
2
|
import { Check } from "typebox/value";
|
|
3
|
-
import { assembleStatusObservation,
|
|
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 =
|
|
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 ?
|
|
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
|
};
|