@compforge/harness-common 0.4.0 → 0.5.3

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
@@ -27,6 +27,12 @@ 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
+ `FileDataSource` is an abstract, runtime-recognizable DataSource for one file path. Its `FileClient.read`
31
+ returns fresh `Uint8Array` bytes with explicit `maxBytes`, `timeoutMs` and optional cancellation.
32
+ An empty file succeeds; an oversized or incomplete read rejects. The source's `path` and concrete
33
+ location metadata let consumers attach provenance. Parsing, configuration precedence and persistence
34
+ belong to consumers. Platform implementations live in `@compforge/harness-toolbox/file`.
35
+
30
36
  Service identifies a Component's runtime presence in an Environment. A Component belongs to a Repository
31
37
  on a Forge. Product is a separate business identity; consumers own its many-to-many associations
32
38
  with Components. Repository discovery and directory ownership remain consumer responsibilities.
@@ -34,20 +40,26 @@ A Service explicitly declares zero or more Workloads. Consumers extend Service w
34
40
  capabilities, without duplicating these identities. For example:
35
41
 
36
42
  ```typescript
37
- import type { Service } from "@compforge/harness-common";
43
+ import {
44
+ repositoryId, conceptToMarkdown, knowledgeToMarkdown, stepToMarkdown, workflowToMarkdown,
45
+ type Service,
46
+ } from "@compforge/harness-common";
38
47
 
39
48
  const service: Service = {
40
49
  name: "chat",
41
50
  knowledge: {
42
- concepts: [{ id: "conversation", name: "Conversation", description: "Contains Messages; each Message may have multiple execution Attempts." }],
43
- workflows: [{ id: "reply", title: "Reply", summary: "Accept and answer a message.", steps: [
44
- { id: "execute", title: "Execute", description: "Execute an attempt and persist its response." },
51
+ markdown() { return knowledgeToMarkdown(this); },
52
+ concepts: [{ id: "conversation", name: "Conversation", description: "Contains Messages; each Message may have multiple execution Attempts.",
53
+ markdown() { return conceptToMarkdown(this); } }],
54
+ workflows: [{ markdown() { return workflowToMarkdown(this); }, id: "reply", title: "Reply", summary: "Accept and answer a message.", steps: [
55
+ { id: "execute", title: "Execute", description: "Execute an attempt and persist its response.",
56
+ markdown() { return stepToMarkdown(this); } },
45
57
  ] }],
46
58
  },
47
59
  upstream: ["gateway"],
48
60
  component: {
49
61
  name: "api",
50
- repository: { forge: { name: "github" }, path: "example/app" },
62
+ repository: { forge: { name: "github" }, path: "example/app", id() { return repositoryId(this); } },
51
63
  },
52
64
  environment: { name: "test" },
53
65
  workloads: [{
@@ -57,6 +69,10 @@ const service: Service = {
57
69
  };
58
70
  ```
59
71
 
72
+ Repository extends Identity: `id()` returns `forge.name/path`, excluding `forkedFrom`.
73
+ Use `repositoryId(this)` when implementing the method, including when restoring a Repository
74
+ from JSON; methods are not serialized.
75
+
60
76
  Service and Product accept an optional description for a short introduction, and structured
61
77
  `knowledge` for concepts, workflows, articles and dictionaries. Service knowledge covers its own
62
78
  responsibilities, while Product knowledge covers end-to-end flows. Consumers maintain this offline
@@ -85,8 +101,15 @@ Development: bun install, make lint, make test. Published artifacts are Node-com
85
101
 
86
102
  Product and Service accept an optional `knowledge` declaration. `Knowledge` groups
87
103
  `Concept`, `Workflow` (with parent-scoped `Step` records), `Article` (with `Section`
88
- records), and typed `Dictionary<Entry>` collections. These are plain offline data:
89
- JSON preserves their content, and the containing consumer owns distribution and version selection.
104
+ records), and typed `Dictionary<Entry>` collections. All eight content interfaces extend
105
+ `Markdown`, whose `markdown(): string` method returns an I/O-free representation.
106
+ Declarations remain ordinary objects; methods can delegate to the shared converters below.
107
+ `KnowledgeSource` describes provenance and does not carry this rendering contract.
108
+
109
+ JSON preserves the content fields, but not methods. Consumers own source validation,
110
+ method restoration, distribution and version selection. Parsing JSON or asserting a type
111
+ does not create a callable Knowledge object; restore methods on each content object before
112
+ exposing it through these interfaces.
90
113
 
91
114
  IDs are unique per kind within the owner, and per child collection within its parent.
92
115
  Step `parentId` expresses inclusion; sibling order follows the array even when a parent
@@ -98,12 +121,49 @@ published definitions independently of a containing distribution's version.
98
121
  `knowledge.ts` also provides deterministic, I/O-free Markdown projections:
99
122
  `knowledgeToMarkdown`, `conceptToMarkdown`, `workflowToMarkdown`, `stepToMarkdown`, `articleToMarkdown`,
100
123
  `sectionToMarkdown`, `dictionaryToMarkdown` and `entryToMarkdown`.
101
- Dictionary entries retain their typed domain fields; consumers can supply an entry
102
- renderer to include these fields. The default renderer uses name, description and aliases.
103
- Terminal presentation, exact lookup, search, source loading and vector indexes belong to consumers.
124
+ Dictionary entries retain their typed domain fields; implement `Entry.markdown()` to
125
+ include these fields. The shared `entryToMarkdown` uses name, description and aliases.
126
+ Generated headings and ordered steps use compact spacing; authored paragraphs and code
127
+ blocks retain their content. Terminal presentation, exact lookup, search, source loading
128
+ and vector indexes belong to consumers.
104
129
 
105
130
  ## Upgrade to 0.4
106
131
 
107
132
  Move the removed Product/Service `concepts` and `workflows` string fields into
108
133
  `knowledge.concepts` and `knowledge.workflows`. Use Concept records for domain objects
109
134
  and Workflow/Step records for business processes. Knowledge remains optional.
135
+
136
+ ## Upgrade to 0.5
137
+
138
+ Implement `markdown()` on Knowledge and each contained Concept, Workflow, Step, Article,
139
+ Section, Dictionary and Entry. For example:
140
+
141
+ ```typescript
142
+ import { conceptToMarkdown, type Concept } from "@compforge/harness-common";
143
+
144
+ const concept: Concept = {
145
+ id: "carrier",
146
+ name: "Carrier",
147
+ description: "Infrastructure capacity.",
148
+ markdown() { return conceptToMarkdown(this); },
149
+ };
150
+
151
+ console.log(concept.markdown());
152
+ ```
153
+
154
+ Existing `...ToMarkdown` functions remain available as shared implementations. Each
155
+ parent calls its children's `markdown()` methods: Knowledge groups concepts, workflows,
156
+ articles and dictionaries; Article renders Sections; Dictionary renders Entries; Workflow
157
+ numbers and indents Steps according to `parentId`. Parents preserve child output instead
158
+ of rebuilding it from the child's fields. Helpers never call the receiver's own method,
159
+ so `markdown() { return knowledgeToMarkdown(this); }` does not recurse.
160
+
161
+ Default headings follow the composition tree: Knowledge categories use level 1; Concepts,
162
+ Workflows, Articles and Dictionaries use level 2; Sections and Entries use level 3.
163
+ Steps return a bold title and body, with numbering supplied by Workflow. Standalone output
164
+ uses the same format. Custom methods own their heading choices; parents do not rewrite
165
+ headings or code blocks in child output.
166
+
167
+ Move `KnowledgeMarkdownOptions.renderEntry` or `dictionaryToMarkdown` renderer callbacks
168
+ into each Entry's `markdown()` method. These callback arguments are removed in 0.5; a
169
+ custom Entry now renders consistently when shown alone or through its Dictionary and Knowledge.
package/dist/file.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ import type { Client } from "./client.js";
2
+ import type { ClientManager } from "./client-manager.js";
3
+ import type { DataSource } from "./datasource.js";
4
+ export interface FileReadOptions {
5
+ /** Maximum complete file size; exceeding it rejects instead of returning a prefix. */
6
+ maxBytes: number;
7
+ /** Includes time waiting for a concurrency slot. */
8
+ timeoutMs: number;
9
+ signal?: AbortSignal;
10
+ }
11
+ /**
12
+ * @spec Each read obtains fresh bytes from the source's bound path. Empty files succeed;
13
+ * missing files, incomplete reads and exceeded limits reject.
14
+ * @rule Parsing, configuration precedence and evidence persistence belong to consumers.
15
+ */
16
+ export interface FileClient extends Client {
17
+ read(options: FileReadOptions): Promise<Uint8Array>;
18
+ }
19
+ /**
20
+ * A single file location, discoverable without creating a client or performing I/O.
21
+ * @why A shared runtime type lets consumers recognize file sources without a second kind registry.
22
+ * @rule clientKey identifies access, not cached file contents or a Service-owned alias.
23
+ * @see {@link component://../../../docs/toolbox.md}
24
+ */
25
+ export declare abstract class FileDataSource<C extends FileClient = FileClient> implements DataSource<C> {
26
+ abstract readonly path: string;
27
+ readonly description?: string;
28
+ abstract readonly clientKey: string;
29
+ abstract createClient(clients: Pick<ClientManager, "get">, signal: AbortSignal): C;
30
+ }
package/dist/file.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * A single file location, discoverable without creating a client or performing I/O.
3
+ * @why A shared runtime type lets consumers recognize file sources without a second kind registry.
4
+ * @rule clientKey identifies access, not cached file contents or a Service-owned alias.
5
+ * @see {@link component://../../../docs/toolbox.md}
6
+ */
7
+ export class FileDataSource {
8
+ description;
9
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * An object referenced by a string identifier; the model owns its format and uniqueness scope.
3
+ * @spec The same business identity yields the same identifier; descriptive metadata is excluded.
4
+ */
5
+ export interface Identity {
6
+ id(): string;
7
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.d.ts CHANGED
@@ -13,3 +13,5 @@ export * from "./service.js";
13
13
  export * from "./execution.js";
14
14
  export type { Product } from "./product.js";
15
15
  export * from "./knowledge.js";
16
+ export type { Identity } from "./identity.js";
17
+ export * from "./file.js";
package/dist/index.js CHANGED
@@ -12,3 +12,4 @@ export * from "./environment.js";
12
12
  export * from "./service.js";
13
13
  export * from "./execution.js";
14
14
  export * from "./knowledge.js";
15
+ export * from "./file.js";
@@ -1,17 +1,24 @@
1
+ /**
2
+ * @spec Knowledge objects provide an I/O-free Markdown representation of their content.
3
+ * @rule JSON preserves content only; consumers restore methods when loading declarations.
4
+ */
5
+ export interface Markdown {
6
+ markdown(): string;
7
+ }
1
8
  /** A named domain object and its relationships or constraints. */
2
- export interface Concept {
9
+ export interface Concept extends Markdown {
3
10
  readonly id: string;
4
11
  readonly name: string;
5
12
  readonly description: string;
6
13
  }
7
14
  /** A documented business process; steps are descriptions, not executable actions. */
8
- export interface Workflow {
15
+ export interface Workflow extends Markdown {
9
16
  readonly id: string;
10
17
  readonly title: string;
11
18
  readonly summary: string;
12
19
  readonly steps: readonly Step[];
13
20
  }
14
- export interface Step {
21
+ export interface Step extends Markdown {
15
22
  readonly id: string;
16
23
  /** Inclusion within this Workflow. Array order determines sibling order. */
17
24
  readonly parentId?: string;
@@ -19,20 +26,20 @@ export interface Step {
19
26
  readonly description: string;
20
27
  }
21
28
  /** A self-contained explanation of a topic. */
22
- export interface Article {
29
+ export interface Article extends Markdown {
23
30
  readonly id: string;
24
31
  readonly title: string;
25
32
  readonly summary: string;
26
33
  readonly sections: readonly Section[];
27
34
  }
28
- export interface Section {
35
+ export interface Section extends Markdown {
29
36
  readonly id: string;
30
37
  readonly title: string;
31
38
  /** Markdown authored by the knowledge owner. */
32
39
  readonly body: string;
33
40
  }
34
41
  /** Common reading identity; consumers extend entries with their typed domain fields. */
35
- export interface Entry {
42
+ export interface Entry extends Markdown {
36
43
  readonly id: string;
37
44
  readonly name: string;
38
45
  readonly description: string;
@@ -45,7 +52,7 @@ export interface KnowledgeSource {
45
52
  readonly reference?: string;
46
53
  }
47
54
  /** Entries retain domain fields. Names and codes need not be unique; IDs must be. */
48
- export interface Dictionary<T extends Entry = Entry> {
55
+ export interface Dictionary<T extends Entry = Entry> extends Markdown {
49
56
  readonly id: string;
50
57
  readonly title: string;
51
58
  readonly summary: string;
@@ -57,7 +64,7 @@ export interface Dictionary<T extends Entry = Entry> {
57
64
  * @rule Top-level IDs are unique per kind; child IDs are unique within their containing object.
58
65
  * @see {@link component://docs/kernel.md}
59
66
  */
60
- export interface Knowledge {
67
+ export interface Knowledge extends Markdown {
61
68
  readonly concepts?: readonly Concept[];
62
69
  readonly workflows?: readonly Workflow[];
63
70
  readonly articles?: readonly Article[];
@@ -73,14 +80,14 @@ export declare function validateKnowledge(knowledge: Knowledge): void;
73
80
  export declare function conceptToMarkdown(concept: Concept): string;
74
81
  export declare function sectionToMarkdown(section: Section): string;
75
82
  export declare function articleToMarkdown(article: Article): string;
76
- export declare function stepToMarkdown(step: Step): string;
77
83
  export declare function workflowToMarkdown(workflow: Workflow): string;
84
+ export declare function stepToMarkdown(step: Step): string;
78
85
  export declare function entryToMarkdown(entry: Entry): string;
79
- /** Domain renderers can include fields beyond the common entry reading contract. */
80
- export declare function dictionaryToMarkdown<T extends Entry>(dictionary: Dictionary<T>, renderEntry?: (entry: T) => string): string;
81
- export interface KnowledgeMarkdownOptions {
82
- /** Consumers own domain-specific entry formatting; omitted renderers use name/description/aliases. */
83
- readonly renderEntry?: (dictionary: Dictionary, entry: Entry) => string;
84
- }
85
- /** Deterministic Markdown projection. It performs no I/O and does not mutate declarations. */
86
- export declare function knowledgeToMarkdown(knowledge: Knowledge, options?: KnowledgeMarkdownOptions): string;
86
+ /** Entries own their domain-specific output; the dictionary owns its title, source and order. */
87
+ export declare function dictionaryToMarkdown<T extends Entry>(dictionary: Dictionary<T>): string;
88
+ /**
89
+ * @spec Parents compose child markdown() output; only the child owns its content rendering.
90
+ * @rule Helpers never invoke the receiver's own markdown(), so declarations can safely delegate to them.
91
+ * @case id=knowledge_composition,desc=`Nested objects customize Markdown`,expect=`Root output preserves each child result`
92
+ */
93
+ export declare function knowledgeToMarkdown(knowledge: Knowledge): string;
package/dist/knowledge.js CHANGED
@@ -55,16 +55,16 @@ function paragraphs(...parts) {
55
55
  return parts.filter(part => part !== undefined && part.length > 0).join("\n\n");
56
56
  }
57
57
  export function conceptToMarkdown(concept) {
58
- return paragraphs(heading(concept.name, 1), concept.description);
58
+ return `${heading(concept.name, 2)}\n${concept.description}`;
59
59
  }
60
60
  export function sectionToMarkdown(section) {
61
- return paragraphs(heading(section.title, 2), section.body);
61
+ return `${heading(section.title, 3)}\n${section.body}`;
62
62
  }
63
63
  export function articleToMarkdown(article) {
64
64
  requireIds(`article '${article.id}'`, article.sections);
65
- return paragraphs(heading(article.title, 1), article.summary, ...article.sections.map(sectionToMarkdown));
65
+ return paragraphs(`${heading(article.title, 2)}\n${article.summary}`, ...article.sections.map(section => section.markdown()));
66
66
  }
67
- function workflowMarkdown(workflow, level) {
67
+ export function workflowToMarkdown(workflow) {
68
68
  validateWorkflow(workflow);
69
69
  const children = new Map();
70
70
  for (const step of workflow.steps) {
@@ -76,41 +76,36 @@ function workflowMarkdown(workflow, level) {
76
76
  return (children.get(parentId) ?? []).map((step, index) => {
77
77
  const marker = `${index + 1}. `;
78
78
  const childIndent = indent + " ".repeat(marker.length);
79
- const body = step.description.split("\n").map(line => childIndent + line).join("\n");
80
- return paragraphs(`${indent}${marker}**${step.title}**`, body, render(step.id, childIndent));
81
- }).join("\n\n");
79
+ const body = step.markdown().split("\n").map((line, lineIndex) => lineIndex === 0 ? `${indent}${marker}${line}` : `${childIndent}${line}`).join("\n");
80
+ return [body, render(step.id, childIndent)]
81
+ .filter(part => part.length > 0).join("\n");
82
+ }).join("\n");
82
83
  };
83
- return paragraphs(heading(workflow.title, level), workflow.summary, render(undefined, ""));
84
+ return paragraphs(`${heading(workflow.title, 2)}\n${workflow.summary}`, render(undefined, ""));
84
85
  }
85
86
  export function stepToMarkdown(step) {
86
- return paragraphs(heading(step.title, 1), step.description);
87
- }
88
- export function workflowToMarkdown(workflow) {
89
- return workflowMarkdown(workflow, 1);
87
+ // Preserve a block boundary before authored descriptions such as indented code and thematic breaks.
88
+ return paragraphs(`**${step.title}**`, step.description);
90
89
  }
91
90
  export function entryToMarkdown(entry) {
92
- return paragraphs(heading(entry.name, 2), entry.description, entry.aliases?.length ? `Aliases: ${entry.aliases.join(", ")}` : undefined);
91
+ return paragraphs(`${heading(entry.name, 3)}\n${entry.description}`, entry.aliases?.length ? `Aliases: ${entry.aliases.join(", ")}` : undefined);
93
92
  }
94
- /** Domain renderers can include fields beyond the common entry reading contract. */
95
- export function dictionaryToMarkdown(dictionary, renderEntry = entryToMarkdown) {
93
+ /** Entries own their domain-specific output; the dictionary owns its title, source and order. */
94
+ export function dictionaryToMarkdown(dictionary) {
96
95
  requireIds(`dictionary '${dictionary.id}'`, dictionary.entries);
97
96
  const source = dictionary.source;
98
- return paragraphs(heading(dictionary.title, 1), dictionary.summary, source ? `Source: ${source.name}@${source.version}${source.reference ? `\n\n${source.reference}` : ""}` : undefined, ...dictionary.entries.map(entry => renderEntry(entry)));
97
+ return paragraphs(`${heading(dictionary.title, 2)}\n${dictionary.summary}`, source ? `Source: ${source.name}@${source.version}${source.reference ? `\n\n${source.reference}` : ""}` : undefined, ...dictionary.entries.map(entry => entry.markdown()));
99
98
  }
100
- function articleMarkdown(article, level) {
101
- return paragraphs(heading(article.title, level), article.summary, ...article.sections.map(section => paragraphs(heading(section.title, level + 1), section.body)));
102
- }
103
- /** Deterministic Markdown projection. It performs no I/O and does not mutate declarations. */
104
- export function knowledgeToMarkdown(knowledge, options = {}) {
99
+ /**
100
+ * @spec Parents compose child markdown() output; only the child owns its content rendering.
101
+ * @rule Helpers never invoke the receiver's own markdown(), so declarations can safely delegate to them.
102
+ * @case id=knowledge_composition,desc=`Nested objects customize Markdown`,expect=`Root output preserves each child result`
103
+ */
104
+ export function knowledgeToMarkdown(knowledge) {
105
105
  validateKnowledge(knowledge);
106
106
  const concepts = knowledge.concepts ?? [];
107
107
  const workflows = knowledge.workflows ?? [];
108
108
  const articles = knowledge.articles ?? [];
109
109
  const dictionaries = knowledge.dictionaries ?? [];
110
- return paragraphs(concepts.length ? paragraphs("## Concepts", ...concepts.map(concept => paragraphs(heading(concept.name, 3), concept.description))) : undefined, workflows.length ? paragraphs("## Workflows", ...workflows.map(workflow => workflowMarkdown(workflow, 3))) : undefined, articles.length ? paragraphs("## Articles", ...articles.map(article => articleMarkdown(article, 3))) : undefined, dictionaries.length ? paragraphs("## Dictionaries", ...dictionaries.map(dictionary => {
111
- const source = dictionary.source;
112
- return paragraphs(heading(dictionary.title, 3), dictionary.summary, source ? `Source: ${source.name}@${source.version}${source.reference ? `\n\n${source.reference}` : ""}` : undefined, ...dictionary.entries.map(entry => options.renderEntry
113
- ? options.renderEntry(dictionary, entry)
114
- : paragraphs(heading(entry.name, 4), entry.description, entry.aliases?.length ? `Aliases: ${entry.aliases.join(", ")}` : undefined)));
115
- })) : undefined);
110
+ return paragraphs(concepts.length ? paragraphs("# Concepts", ...concepts.map(concept => concept.markdown())) : undefined, workflows.length ? paragraphs("# Workflows", ...workflows.map(workflow => workflow.markdown())) : undefined, articles.length ? paragraphs("# Articles", ...articles.map(article => article.markdown())) : undefined, dictionaries.length ? paragraphs("# Dictionaries", ...dictionaries.map(dictionary => dictionary.markdown())) : undefined);
116
111
  }
@@ -1,6 +1,21 @@
1
1
  import type { Forge } from "./forge.js";
2
- /** A repository identified by its Forge and path within that Forge. */
3
- export interface Repository {
2
+ import type { Identity } from "./identity.js";
3
+ /**
4
+ * A repository identified by its Forge and path within that Forge.
5
+ * @spec forkedFrom is the direct parent path within the same Forge instance, not repository identity.
6
+ * @see {@link component://docs/kernel.md}
7
+ */
8
+ export interface Repository extends Identity {
4
9
  readonly forge: Forge;
5
10
  readonly path: string;
11
+ /**
12
+ * Direct parent repository path within this same Forge instance; omission/null is unspecified.
13
+ * Cross-Forge import/mirror provenance belongs in separate metadata.
14
+ */
15
+ readonly forkedFrom?: string | null;
6
16
  }
17
+ /**
18
+ * Derive the Repository.id() value from identity fields.
19
+ * @spec Repository identity is forge.name/path; fork provenance is excluded.
20
+ */
21
+ export declare function repositoryId(repository: Pick<Repository, "forge" | "path">): string;
@@ -1 +1,7 @@
1
- export {};
1
+ /**
2
+ * Derive the Repository.id() value from identity fields.
3
+ * @spec Repository identity is forge.name/path; fork provenance is excluded.
4
+ */
5
+ export function repositoryId(repository) {
6
+ return `${repository.forge.name}/${repository.path}`;
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compforge/harness-common",
3
- "version": "0.4.0",
3
+ "version": "0.5.3",
4
4
  "description": "Execution-neutral client lifecycle and datasource contracts for harnesses",
5
5
  "type": "module",
6
6
  "scripts": {