@compforge/harness-common 0.5.10 → 0.5.11

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
@@ -195,3 +195,11 @@ rendering remain consumer-owned. See [the shared contract](../../../docs/resourc
195
195
  State, branches, source repository, revisions and provider timestamps are optional facts.
196
196
  See [the shared contract](../../../docs/pull-request.md) for identity and unknown-state semantics.
197
197
  Forge access and polling remain consumer/toolkit responsibilities.
198
+
199
+ ## Portable values
200
+
201
+ `Value`, `ValueCodec`, `Summary`, and JSON normalization are producer-owned data contracts.
202
+ Use `serializeValue` to preserve immutable JSON and reading hints, and `deserializeValue`
203
+ with an explicit matching codec to restore runtime behavior. Encoding and decoding are pure.
204
+ Doctor and its Plugins import these contracts directly from `@compforge/harness-common`;
205
+ Observation identity, evidence and rendering remain with their consumers.
package/dist/index.d.ts CHANGED
@@ -17,3 +17,6 @@ export * from "./system/knowledge.js";
17
17
  export type { Identity } from "./identity.js";
18
18
  export * from "./access/file.js";
19
19
  export * from "./system/resource.js";
20
+ export * from "./json.js";
21
+ export * from "./summary.js";
22
+ export * from "./value.js";
package/dist/index.js CHANGED
@@ -15,3 +15,6 @@ export * from "./execution.js";
15
15
  export * from "./system/knowledge.js";
16
16
  export * from "./access/file.js";
17
17
  export * from "./system/resource.js";
18
+ export * from "./json.js";
19
+ export * from "./summary.js";
20
+ export * from "./value.js";
package/dist/json.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ /** Normalized protocol data: object absence is represented by an omitted property. */
2
+ export type JsonPrimitive = null | boolean | number | string;
3
+ export type JsonValue = JsonPrimitive | readonly JsonValue[] | JsonObject;
4
+ export type JsonObject = {
5
+ readonly [key: string]: JsonValue;
6
+ };
7
+ /** Encoder input may express absent object fields as undefined; array elements must have a value. */
8
+ export type JsonInput = JsonPrimitive | readonly JsonInput[] | {
9
+ readonly [key: string]: JsonInput | undefined;
10
+ };
11
+ /**
12
+ * @spec Undefined object properties mean absence and are omitted recursively; null remains explicit.
13
+ * @rule Top-level undefined, undefined array elements and holes are rejected, never converted to null.
14
+ * @see ../../../../docs/kernel.md
15
+ */
16
+ export declare function immutableJsonValue(value: unknown, label: string): JsonValue;
17
+ /** Normalize optional fields and create a deeply immutable JSON object without coercing unsupported data. */
18
+ export declare function immutableJsonObject(value: unknown, label: string): JsonObject;
package/dist/json.js ADDED
@@ -0,0 +1,79 @@
1
+ function propertyPath(path, key) {
2
+ return `${path}[${JSON.stringify(key)}]`;
3
+ }
4
+ function cloneJson(value, path, ancestors) {
5
+ if (value === null || typeof value === "string" || typeof value === "boolean")
6
+ return value;
7
+ if (typeof value === "number") {
8
+ if (!Number.isFinite(value))
9
+ throw new Error(`${path} must be a finite JSON number`);
10
+ return Object.is(value, -0) ? 0 : value;
11
+ }
12
+ if (typeof value !== "object") {
13
+ throw new Error(`${path} contains unsupported JSON value '${typeof value}'`);
14
+ }
15
+ if (ancestors.has(value))
16
+ throw new Error(`${path} contains a circular reference`);
17
+ ancestors.add(value);
18
+ try {
19
+ if (Array.isArray(value)) {
20
+ const copy = [];
21
+ for (let index = 0; index < value.length; index += 1) {
22
+ if (!Object.hasOwn(value, index))
23
+ throw new Error(`${path}[${index}] is a sparse array item`);
24
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
25
+ if (!descriptor.enumerable || !("value" in descriptor)) {
26
+ throw new Error(`${path}[${index}] must be an enumerable data property`);
27
+ }
28
+ copy.push(cloneJson(descriptor.value, `${path}[${index}]`, ancestors));
29
+ }
30
+ for (const key of Reflect.ownKeys(value)) {
31
+ if (key === "length")
32
+ continue;
33
+ if (typeof key !== "string" || !/^(0|[1-9]\d*)$/.test(key) || Number(key) >= value.length) {
34
+ throw new Error(`${path} contains a non-JSON array property`);
35
+ }
36
+ }
37
+ return Object.freeze(copy);
38
+ }
39
+ const prototype = Object.getPrototypeOf(value);
40
+ if (prototype !== Object.prototype && prototype !== null) {
41
+ throw new Error(`${path} must be a plain JSON object`);
42
+ }
43
+ const copy = Object.create(null);
44
+ for (const key of Reflect.ownKeys(value)) {
45
+ if (typeof key !== "string")
46
+ throw new Error(`${path} contains a symbol property`);
47
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
48
+ if (!descriptor.enumerable || !("value" in descriptor)) {
49
+ throw new Error(`${propertyPath(path, key)} must be an enumerable data property`);
50
+ }
51
+ if (descriptor.value === undefined)
52
+ continue;
53
+ copy[key] = cloneJson(descriptor.value, propertyPath(path, key), ancestors);
54
+ }
55
+ return Object.freeze(copy);
56
+ }
57
+ finally {
58
+ ancestors.delete(value);
59
+ }
60
+ }
61
+ function isJsonArray(value) {
62
+ return Array.isArray(value);
63
+ }
64
+ /**
65
+ * @spec Undefined object properties mean absence and are omitted recursively; null remains explicit.
66
+ * @rule Top-level undefined, undefined array elements and holes are rejected, never converted to null.
67
+ * @see ../../../../docs/kernel.md
68
+ */
69
+ export function immutableJsonValue(value, label) {
70
+ return cloneJson(value, label, new Set());
71
+ }
72
+ /** Normalize optional fields and create a deeply immutable JSON object without coercing unsupported data. */
73
+ export function immutableJsonObject(value, label) {
74
+ const snapshot = immutableJsonValue(value, label);
75
+ if (snapshot === null || isJsonArray(snapshot) || typeof snapshot !== "object") {
76
+ throw new Error(`${label} must be a JSON object`);
77
+ }
78
+ return snapshot;
79
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Producer-owned reading hints for the accompanying evidence.
3
+ *
4
+ * @spec text explains what the evidence means and limits on judgment; fields expose supporting counts, objects, and evidence.
5
+ * @spec The enclosing command or query result owns status: it describes execution completeness, not application health.
6
+ * @rule For partial results, truncation, zero matches, or failed sources, explain what was obtained, what is missing, and which judgments remain unsupported.
7
+ * @see ../../../../docs/kernel.md
8
+ */
9
+ export interface Summary {
10
+ readonly title: string;
11
+ /**
12
+ * A few concise, producer-authored sentences interpreting the result and its limits.
13
+ * Keep data details, record lists, and raw output in fields or linked evidence; text is not a detailed report.
14
+ * Avoid repeating field values or inferring unsupported conclusions; bounded views may truncate long text.
15
+ * Optional when the fields are self-explanatory; the accompanying data remains the machine contract.
16
+ */
17
+ readonly text?: string;
18
+ /** Supporting evidence in reading order, with decisive facts before context for bounded views. Paths are relative to the accompanying data. */
19
+ readonly fields: readonly {
20
+ readonly label: string;
21
+ readonly path: readonly string[];
22
+ }[];
23
+ }
24
+ export declare function validateSummary(value: unknown): asserts value is Summary;
@@ -0,0 +1,17 @@
1
+ export function validateSummary(value) {
2
+ if (!value || typeof value !== "object" || Array.isArray(value))
3
+ throw new Error("Summary must be an object");
4
+ const summary = value;
5
+ if (typeof summary.title !== "string" || !summary.title.trim())
6
+ throw new Error("Summary.title must be a non-empty string");
7
+ if (summary.text !== undefined && typeof summary.text !== "string")
8
+ throw new Error("Summary.text must be a string");
9
+ if (!Array.isArray(summary.fields))
10
+ throw new Error("Summary.fields must be an array");
11
+ for (const field of summary.fields) {
12
+ if (!field || typeof field !== "object" || typeof field.label !== "string" || !field.label.trim()
13
+ || !Array.isArray(field.path) || !field.path.every((key) => typeof key === "string" && key.length > 0)) {
14
+ throw new Error("Summary field requires a label and a string path");
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,27 @@
1
+ import { type JsonInput, type JsonValue } from "./json.js";
2
+ import { type Summary } from "./summary.js";
3
+ /** Runtime business data and its producer-owned reading hints. */
4
+ export interface Value {
5
+ summary(): Summary;
6
+ }
7
+ /**
8
+ * @spec decode validates unknown JSON and restores equivalent data and behavior after encode
9
+ * @rule Both operations are pure: no environment access or resource acquisition
10
+ * @see ../../../../docs/kernel.md
11
+ */
12
+ export interface ValueCodec<T extends Value> {
13
+ encode(value: T): JsonInput;
14
+ decode(json: unknown): T;
15
+ }
16
+ /** A portable representation of a producer-owned Value, independent of any Command or Observation. */
17
+ export interface SerializedValue {
18
+ readonly data: JsonValue;
19
+ readonly summary: Summary;
20
+ }
21
+ /** @spec Serialization normalizes absent object fields and preserves protocol data; the Codec owns domain validation and reconstruction. */
22
+ export declare function serializeValue<T extends Value>(value: T, codec: ValueCodec<T>, options?: {
23
+ label?: string;
24
+ maxBytes?: number;
25
+ }): SerializedValue;
26
+ /** @spec Rehydration is explicit and pure; it never replaces a previously saved Summary. */
27
+ export declare function deserializeValue<T extends Value>(json: unknown, codec: ValueCodec<T>): T;
package/dist/value.js ADDED
@@ -0,0 +1,21 @@
1
+ import { immutableJsonObject, immutableJsonValue } from "./json.js";
2
+ import { validateSummary } from "./summary.js";
3
+ /** @spec Serialization normalizes absent object fields and preserves protocol data; the Codec owns domain validation and reconstruction. */
4
+ export function serializeValue(value, codec, options = {}) {
5
+ const label = options.label ?? "Value";
6
+ const data = immutableJsonValue(codec.encode(value), `${label}.data`);
7
+ if (options.maxBytes !== undefined && new TextEncoder().encode(JSON.stringify(data)).byteLength > options.maxBytes) {
8
+ throw new Error(`${label} exceeds JSON byte budget`);
9
+ }
10
+ const restored = deserializeValue(data, codec);
11
+ const summary = restored.summary();
12
+ validateSummary(summary);
13
+ return Object.freeze({ data, summary: immutableJsonObject(summary, `${label}.summary`) });
14
+ }
15
+ /** @spec Rehydration is explicit and pure; it never replaces a previously saved Summary. */
16
+ export function deserializeValue(json, codec) {
17
+ const value = codec.decode(immutableJsonValue(json, "Value.data"));
18
+ if (!value || typeof value.summary !== "function")
19
+ throw new Error("Codec must restore a Value with summary()");
20
+ return value;
21
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compforge/harness-common",
3
- "version": "0.5.10",
3
+ "version": "0.5.11",
4
4
  "description": "Execution-neutral client lifecycle and datasource contracts for harnesses",
5
5
  "type": "module",
6
6
  "scripts": {