@compforge/harness-common 0.5.9 → 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 +13 -0
- package/dist/access/client-manager.d.ts +6 -0
- package/dist/access/client-manager.js +11 -1
- package/dist/access/client.d.ts +6 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/json.d.ts +18 -0
- package/dist/json.js +79 -0
- package/dist/summary.d.ts +24 -0
- package/dist/summary.js +17 -0
- package/dist/value.d.ts +27 -0
- package/dist/value.js +21 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,6 +27,11 @@ view during initialize, so concurrent consumers share them and dispose before th
|
|
|
27
27
|
Dependency graphs must be acyclic. A failed initialization permits retry only after successful cleanup;
|
|
28
28
|
failed cleanup poisons that key and is reported again when the root manager is disposed.
|
|
29
29
|
|
|
30
|
+
Use `clients.create({ createClient })` for a fresh caller-owned client. The caller may dispose it as soon as its work
|
|
31
|
+
finishes; dependencies acquired through `get` remain available to other clients until root disposal. The root also
|
|
32
|
+
disposes created clients as an idempotent safety net. Use `get` when callers should share the client itself; borrowers
|
|
33
|
+
must not dispose those shared instances. Neither API constructs anything before it is called.
|
|
34
|
+
|
|
30
35
|
`FileDataSource` declares a nonempty group of distinct `paths` that can be acquired together.
|
|
31
36
|
`FileClient.read` obtains the whole group once and returns one `FileReadResult` per declared path,
|
|
32
37
|
in order: `{ path, status: "ok", bytes }` or `{ path, status: "error", error }`. Empty files succeed;
|
|
@@ -190,3 +195,11 @@ rendering remain consumer-owned. See [the shared contract](../../../docs/resourc
|
|
|
190
195
|
State, branches, source repository, revisions and provider timestamps are optional facts.
|
|
191
196
|
See [the shared contract](../../../docs/pull-request.md) for identity and unknown-state semantics.
|
|
192
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.
|
|
@@ -9,5 +9,11 @@ export declare class ClientManager {
|
|
|
9
9
|
readonly signal: AbortSignal;
|
|
10
10
|
constructor(signal?: AbortSignal);
|
|
11
11
|
get<C extends Client>(provider: ClientProvider<C>): Promise<C>;
|
|
12
|
+
/**
|
|
13
|
+
* @spec Create a caller-owned client that can be disposed before this root scope ends.
|
|
14
|
+
* @rule Dependencies acquired with get remain shared until root disposal; create never caches by identity.
|
|
15
|
+
* @why Short-lived protocol clients borrow longer-lived transports. Root disposal remains an idempotent safety net.
|
|
16
|
+
*/
|
|
17
|
+
create<C extends Client>(factory: Pick<ClientProvider<C>, "createClient">): Promise<C>;
|
|
12
18
|
dispose(): Promise<void>;
|
|
13
19
|
}
|
|
@@ -14,8 +14,18 @@ export class ClientManager {
|
|
|
14
14
|
this.signal = signal ? AbortSignal.any([signal, this.#controller.signal]) : this.#controller.signal;
|
|
15
15
|
}
|
|
16
16
|
get(provider) {
|
|
17
|
+
return this.#acquire(provider.clientKey, provider);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* @spec Create a caller-owned client that can be disposed before this root scope ends.
|
|
21
|
+
* @rule Dependencies acquired with get remain shared until root disposal; create never caches by identity.
|
|
22
|
+
* @why Short-lived protocol clients borrow longer-lived transports. Root disposal remains an idempotent safety net.
|
|
23
|
+
*/
|
|
24
|
+
create(factory) {
|
|
25
|
+
return this.#acquire(Symbol("owned client"), factory);
|
|
26
|
+
}
|
|
27
|
+
#acquire(key, provider) {
|
|
17
28
|
this.signal.throwIfAborted();
|
|
18
|
-
const key = provider.clientKey;
|
|
19
29
|
const existing = this.#clients.get(key);
|
|
20
30
|
if (existing)
|
|
21
31
|
return existing;
|
package/dist/access/client.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Managed access lifecycle, implemented by protocol clients and resource-owning transports.
|
|
3
|
+
* @spec Initialization failure still requires disposal of every partially created resource.
|
|
4
|
+
* @spec Successful disposal means owned work has ended; borrowed dependencies belong to their manager.
|
|
5
|
+
* @rule A timeout waiting for cleanup is a cleanup failure, never proof that resources were released.
|
|
6
|
+
*/
|
|
2
7
|
export interface Client {
|
|
3
8
|
initialize(): Promise<void>;
|
|
4
9
|
dispose(): Promise<void>;
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
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;
|
package/dist/summary.js
ADDED
|
@@ -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
|
+
}
|
package/dist/value.d.ts
ADDED
|
@@ -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
|
+
}
|