@compforge/harness-common 0.5.10 → 0.5.12
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 +16 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -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/system/component.d.ts +9 -1
- package/dist/system/depot.d.ts +17 -0
- package/dist/system/depot.js +1 -0
- package/dist/system/tool.d.ts +8 -0
- package/dist/system/tool.js +1 -0
- package/dist/value.d.ts +27 -0
- package/dist/value.js +21 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -195,3 +195,19 @@ 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.
|
|
206
|
+
|
|
207
|
+
## Component tools and Depot
|
|
208
|
+
|
|
209
|
+
Component optionally declares `tools` and one logical `depot`. `Tool` describes a development
|
|
210
|
+
tool and an optional declared version/constraint. `Depot.bins` groups `Bin` declarations with
|
|
211
|
+
repository-relative paths and `dependencies`, `output` or `cache` kinds. Metadata does not change
|
|
212
|
+
Component identity, prove physical existence or authorize deletion. Discovery, preparation,
|
|
213
|
+
usage protection and reclamation belong to consumers. See [the shared contract](../../../docs/component-depot.md).
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export * from "./system/forge.js";
|
|
|
8
8
|
export * from "./system/repository.js";
|
|
9
9
|
export * from "./system/pull-request.js";
|
|
10
10
|
export * from "./system/component.js";
|
|
11
|
+
export * from "./system/tool.js";
|
|
12
|
+
export * from "./system/depot.js";
|
|
11
13
|
export * from "./system/host.js";
|
|
12
14
|
export * from "./system/environment.js";
|
|
13
15
|
export * from "./system/service.js";
|
|
@@ -17,3 +19,6 @@ export * from "./system/knowledge.js";
|
|
|
17
19
|
export type { Identity } from "./identity.js";
|
|
18
20
|
export * from "./access/file.js";
|
|
19
21
|
export * from "./system/resource.js";
|
|
22
|
+
export * from "./json.js";
|
|
23
|
+
export * from "./summary.js";
|
|
24
|
+
export * from "./value.js";
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,8 @@ export * from "./system/forge.js";
|
|
|
8
8
|
export * from "./system/repository.js";
|
|
9
9
|
export * from "./system/pull-request.js";
|
|
10
10
|
export * from "./system/component.js";
|
|
11
|
+
export * from "./system/tool.js";
|
|
12
|
+
export * from "./system/depot.js";
|
|
11
13
|
export * from "./system/host.js";
|
|
12
14
|
export * from "./system/environment.js";
|
|
13
15
|
export * from "./system/service.js";
|
|
@@ -15,3 +17,6 @@ export * from "./execution.js";
|
|
|
15
17
|
export * from "./system/knowledge.js";
|
|
16
18
|
export * from "./access/file.js";
|
|
17
19
|
export * from "./system/resource.js";
|
|
20
|
+
export * from "./json.js";
|
|
21
|
+
export * from "./summary.js";
|
|
22
|
+
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;
|
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
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import type { Depot } from "./depot.js";
|
|
2
|
+
import type { Tool } from "./tool.js";
|
|
1
3
|
import type { Repository } from "./repository.js";
|
|
2
|
-
/** A buildable component within a repository; one repository may own many.
|
|
4
|
+
/** A buildable component within a repository; one repository may own many.
|
|
5
|
+
* @spec language, tools and depot are optional declarations, not Component identity or runtime evidence.
|
|
6
|
+
*/
|
|
3
7
|
export interface Component {
|
|
4
8
|
readonly repository: Repository;
|
|
5
9
|
readonly name: string;
|
|
@@ -7,6 +11,10 @@ export interface Component {
|
|
|
7
11
|
readonly description?: string;
|
|
8
12
|
/** Optional implementation language, supplied by consumers; not part of identity. */
|
|
9
13
|
readonly language?: string;
|
|
14
|
+
/** Omitted means undeclared; an empty list declares no tools. */
|
|
15
|
+
readonly tools?: readonly Tool[];
|
|
16
|
+
/** Logical content declaration, resolved separately in each checkout. */
|
|
17
|
+
readonly depot?: Depot;
|
|
10
18
|
}
|
|
11
19
|
/** Derive the tool ecosystem from language; unknown mappings return undefined.
|
|
12
20
|
* Package manager discovery and execution remain consumer responsibilities.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** A recoverable content unit within a Depot, usually one directory.
|
|
2
|
+
* @spec path is a normalized repository-relative POSIX path, never checkout-absolute.
|
|
3
|
+
* @rule A declaration alone does not authorize deletion or prove exclusive ownership.
|
|
4
|
+
* @see {@link component://docs/component-depot.md}
|
|
5
|
+
*/
|
|
6
|
+
export interface Bin {
|
|
7
|
+
readonly path: string;
|
|
8
|
+
readonly kind: "dependencies" | "output" | "cache";
|
|
9
|
+
}
|
|
10
|
+
/** The optional logical container for a Component's recoverable supporting content.
|
|
11
|
+
* @spec A Component has at most one Depot per checkout; missing contents do not remove its declaration.
|
|
12
|
+
* @spec Consumers own discovery, preparation, usage protection and reclamation.
|
|
13
|
+
* @see {@link component://docs/component-depot.md}
|
|
14
|
+
*/
|
|
15
|
+
export interface Depot {
|
|
16
|
+
readonly bins: readonly Bin[];
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** A development tool declared for a Component, such as pnpm, uv or Vite.
|
|
2
|
+
* @spec A declaration does not prove installation or select an executable.
|
|
3
|
+
*/
|
|
4
|
+
export interface Tool {
|
|
5
|
+
readonly name: string;
|
|
6
|
+
/** Declared version or constraint, not an observed installed version. */
|
|
7
|
+
readonly version?: string;
|
|
8
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
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
|
+
}
|