@compforge/harness-common 0.3.7 → 0.5.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/README.md CHANGED
@@ -34,12 +34,23 @@ A Service explicitly declares zero or more Workloads. Consumers extend Service w
34
34
  capabilities, without duplicating these identities. For example:
35
35
 
36
36
  ```typescript
37
- import type { Service } from "@compforge/harness-common";
37
+ import {
38
+ conceptToMarkdown, knowledgeToMarkdown, stepToMarkdown, workflowToMarkdown,
39
+ type Service,
40
+ } from "@compforge/harness-common";
38
41
 
39
42
  const service: Service = {
40
43
  name: "chat",
41
- concepts: "A Conversation contains Messages; each Message may have multiple execution Attempts.",
42
- workflows: "Accept message → execute attempt → persist response. Acceptance does not imply completion.",
44
+ knowledge: {
45
+ markdown() { return knowledgeToMarkdown(this); },
46
+ concepts: [{ id: "conversation", name: "Conversation", description: "Contains Messages; each Message may have multiple execution Attempts.",
47
+ markdown() { return conceptToMarkdown(this); } }],
48
+ workflows: [{ markdown() { return workflowToMarkdown(this); }, id: "reply", title: "Reply", summary: "Accept and answer a message.", steps: [
49
+ { id: "execute", title: "Execute", description: "Execute an attempt and persist its response.",
50
+ markdown() { return stepToMarkdown(this); } },
51
+ ] }],
52
+ },
53
+ upstream: ["gateway"],
43
54
  component: {
44
55
  name: "api",
45
56
  repository: { forge: { name: "github" }, path: "example/app" },
@@ -52,11 +63,14 @@ const service: Service = {
52
63
  };
53
64
  ```
54
65
 
55
- Service and Product accept an optional description for a short introduction, plus optional
56
- concepts and workflows Markdown text. Concepts explain
57
- objects, relationships and constraints; workflows explain collaboration from trigger to completion.
58
- Service knowledge covers its own responsibilities, while Product knowledge covers end-to-end flows.
59
- Consumers maintain this offline metadata; it does not change identity, execute a workflow or prove runtime state.
66
+ Service and Product accept an optional description for a short introduction, and structured
67
+ `knowledge` for concepts, workflows, articles and dictionaries. Service knowledge covers its own
68
+ responsibilities, while Product knowledge covers end-to-end flows. Consumers maintain this offline
69
+ metadata; it does not change identity, execute a workflow or prove runtime state.
70
+
71
+ Service `upstream` lists the canonical names of Services that call it, resolved in the consumer's
72
+ catalog. Omission means the relationship is undeclared; `[]` declares no upstream callers.
73
+ This describes architecture; runtime traces determine which calls actually occurred.
60
74
 
61
75
  Environment carries identity and an optional local/SSH Host declaration, not a connection. HostEnvironment
62
76
  describes a target running directly on that host. Concrete accessible environments, such as toolbox's
@@ -72,3 +86,74 @@ Python's corresponding package is sdks/python/common (harness-common). Languages
72
86
  and independently versioned packages; they need not implement every model in lockstep.
73
87
 
74
88
  Development: bun install, make lint, make test. Published artifacts are Node-compatible ESM and declarations.
89
+
90
+ ## Structured knowledge
91
+
92
+ Product and Service accept an optional `knowledge` declaration. `Knowledge` groups
93
+ `Concept`, `Workflow` (with parent-scoped `Step` records), `Article` (with `Section`
94
+ records), and typed `Dictionary<Entry>` collections. All eight content interfaces extend
95
+ `Markdown`, whose `markdown(): string` method returns an I/O-free representation.
96
+ Declarations remain ordinary objects; methods can delegate to the shared converters below.
97
+ `KnowledgeSource` describes provenance and does not carry this rendering contract.
98
+
99
+ JSON preserves the content fields, but not methods. Consumers own source validation,
100
+ method restoration, distribution and version selection. Parsing JSON or asserting a type
101
+ does not create a callable Knowledge object; restore methods on each content object before
102
+ exposing it through these interfaces.
103
+
104
+ IDs are unique per kind within the owner, and per child collection within its parent.
105
+ Step `parentId` expresses inclusion; sibling order follows the array even when a parent
106
+ appears after its child. `validateKnowledge` rejects duplicate/empty IDs, unknown parents
107
+ and parent cycles before references are exposed. Dictionary entry IDs are unique, while
108
+ names and domain keys such as error codes may repeat. `source.version` describes the
109
+ published definitions independently of a containing distribution's version.
110
+
111
+ `knowledge.ts` also provides deterministic, I/O-free Markdown projections:
112
+ `knowledgeToMarkdown`, `conceptToMarkdown`, `workflowToMarkdown`, `stepToMarkdown`, `articleToMarkdown`,
113
+ `sectionToMarkdown`, `dictionaryToMarkdown` and `entryToMarkdown`.
114
+ Dictionary entries retain their typed domain fields; implement `Entry.markdown()` to
115
+ include these fields. The shared `entryToMarkdown` uses name, description and aliases.
116
+ Generated headings and ordered steps use compact spacing; authored paragraphs and code
117
+ blocks retain their content. Terminal presentation, exact lookup, search, source loading
118
+ and vector indexes belong to consumers.
119
+
120
+ ## Upgrade to 0.4
121
+
122
+ Move the removed Product/Service `concepts` and `workflows` string fields into
123
+ `knowledge.concepts` and `knowledge.workflows`. Use Concept records for domain objects
124
+ and Workflow/Step records for business processes. Knowledge remains optional.
125
+
126
+ ## Upgrade to 0.5
127
+
128
+ Implement `markdown()` on Knowledge and each contained Concept, Workflow, Step, Article,
129
+ Section, Dictionary and Entry. For example:
130
+
131
+ ```typescript
132
+ import { conceptToMarkdown, type Concept } from "@compforge/harness-common";
133
+
134
+ const concept: Concept = {
135
+ id: "carrier",
136
+ name: "Carrier",
137
+ description: "Infrastructure capacity.",
138
+ markdown() { return conceptToMarkdown(this); },
139
+ };
140
+
141
+ console.log(concept.markdown());
142
+ ```
143
+
144
+ Existing `...ToMarkdown` functions remain available as shared implementations. Each
145
+ parent calls its children's `markdown()` methods: Knowledge groups concepts, workflows,
146
+ articles and dictionaries; Article renders Sections; Dictionary renders Entries; Workflow
147
+ numbers and indents Steps according to `parentId`. Parents preserve child output instead
148
+ of rebuilding it from the child's fields. Helpers never call the receiver's own method,
149
+ so `markdown() { return knowledgeToMarkdown(this); }` does not recurse.
150
+
151
+ Default headings follow the composition tree: Knowledge categories use level 1; Concepts,
152
+ Workflows, Articles and Dictionaries use level 2; Sections and Entries use level 3.
153
+ Steps return a bold title and body, with numbering supplied by Workflow. Standalone output
154
+ uses the same format. Custom methods own their heading choices; parents do not rewrite
155
+ headings or code blocks in child output.
156
+
157
+ Move `KnowledgeMarkdownOptions.renderEntry` or `dictionaryToMarkdown` renderer callbacks
158
+ into each Entry's `markdown()` method. These callback arguments are removed in 0.5; a
159
+ custom Entry now renders consistently when shown alone or through its Dictionary and Knowledge.
package/dist/index.d.ts CHANGED
@@ -12,3 +12,4 @@ export * from "./environment.js";
12
12
  export * from "./service.js";
13
13
  export * from "./execution.js";
14
14
  export type { Product } from "./product.js";
15
+ export * from "./knowledge.js";
package/dist/index.js CHANGED
@@ -11,3 +11,4 @@ export * from "./host.js";
11
11
  export * from "./environment.js";
12
12
  export * from "./service.js";
13
13
  export * from "./execution.js";
14
+ export * from "./knowledge.js";
@@ -0,0 +1,93 @@
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
+ }
8
+ /** A named domain object and its relationships or constraints. */
9
+ export interface Concept extends Markdown {
10
+ readonly id: string;
11
+ readonly name: string;
12
+ readonly description: string;
13
+ }
14
+ /** A documented business process; steps are descriptions, not executable actions. */
15
+ export interface Workflow extends Markdown {
16
+ readonly id: string;
17
+ readonly title: string;
18
+ readonly summary: string;
19
+ readonly steps: readonly Step[];
20
+ }
21
+ export interface Step extends Markdown {
22
+ readonly id: string;
23
+ /** Inclusion within this Workflow. Array order determines sibling order. */
24
+ readonly parentId?: string;
25
+ readonly title: string;
26
+ readonly description: string;
27
+ }
28
+ /** A self-contained explanation of a topic. */
29
+ export interface Article extends Markdown {
30
+ readonly id: string;
31
+ readonly title: string;
32
+ readonly summary: string;
33
+ readonly sections: readonly Section[];
34
+ }
35
+ export interface Section extends Markdown {
36
+ readonly id: string;
37
+ readonly title: string;
38
+ /** Markdown authored by the knowledge owner. */
39
+ readonly body: string;
40
+ }
41
+ /** Common reading identity; consumers extend entries with their typed domain fields. */
42
+ export interface Entry extends Markdown {
43
+ readonly id: string;
44
+ readonly name: string;
45
+ readonly description: string;
46
+ readonly aliases?: readonly string[];
47
+ }
48
+ export interface KnowledgeSource {
49
+ readonly name: string;
50
+ /** Definition version, independently of a consumer's distribution version. */
51
+ readonly version: string;
52
+ readonly reference?: string;
53
+ }
54
+ /** Entries retain domain fields. Names and codes need not be unique; IDs must be. */
55
+ export interface Dictionary<T extends Entry = Entry> extends Markdown {
56
+ readonly id: string;
57
+ readonly title: string;
58
+ readonly summary: string;
59
+ readonly source?: KnowledgeSource;
60
+ readonly entries: readonly T[];
61
+ }
62
+ /**
63
+ * @spec Knowledge is owner-scoped offline content, independent of runtime evidence and identity.
64
+ * @rule Top-level IDs are unique per kind; child IDs are unique within their containing object.
65
+ * @see {@link component://docs/kernel.md}
66
+ */
67
+ export interface Knowledge extends Markdown {
68
+ readonly concepts?: readonly Concept[];
69
+ readonly workflows?: readonly Workflow[];
70
+ readonly articles?: readonly Article[];
71
+ readonly dictionaries?: readonly Dictionary[];
72
+ }
73
+ /**
74
+ * @spec Parent IDs express inclusion only; missing parents, self-reference and cycles are invalid.
75
+ * @case id=workflow_hierarchy,desc=`Parent appears after child`,expect=`Sibling array order is preserved`
76
+ */
77
+ export declare function validateWorkflow(workflow: Workflow): void;
78
+ /** Validate declaration identities before exposing references or rendering any content. */
79
+ export declare function validateKnowledge(knowledge: Knowledge): void;
80
+ export declare function conceptToMarkdown(concept: Concept): string;
81
+ export declare function sectionToMarkdown(section: Section): string;
82
+ export declare function articleToMarkdown(article: Article): string;
83
+ export declare function workflowToMarkdown(workflow: Workflow): string;
84
+ export declare function stepToMarkdown(step: Step): string;
85
+ export declare function entryToMarkdown(entry: Entry): 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;
@@ -0,0 +1,111 @@
1
+ function requireIds(kind, values) {
2
+ const ids = new Set();
3
+ for (const value of values) {
4
+ if (!value.id.trim())
5
+ throw new Error(`${kind}: ID must not be empty`);
6
+ if (ids.has(value.id))
7
+ throw new Error(`${kind}: duplicate ID '${value.id}'`);
8
+ ids.add(value.id);
9
+ }
10
+ }
11
+ /**
12
+ * @spec Parent IDs express inclusion only; missing parents, self-reference and cycles are invalid.
13
+ * @case id=workflow_hierarchy,desc=`Parent appears after child`,expect=`Sibling array order is preserved`
14
+ */
15
+ export function validateWorkflow(workflow) {
16
+ const label = `workflow '${workflow.id}'`;
17
+ requireIds(label, workflow.steps);
18
+ const steps = new Map(workflow.steps.map(step => [step.id, step]));
19
+ for (const step of workflow.steps) {
20
+ if (step.parentId !== undefined && !steps.has(step.parentId)) {
21
+ throw new Error(`${label}: step '${step.id}' has unknown parent '${step.parentId}'`);
22
+ }
23
+ }
24
+ const visited = new Set();
25
+ for (const step of workflow.steps) {
26
+ const path = new Set();
27
+ let current = step;
28
+ while (current && !visited.has(current.id)) {
29
+ if (path.has(current.id))
30
+ throw new Error(`${label}: parent cycle at step '${current.id}'`);
31
+ path.add(current.id);
32
+ current = current.parentId === undefined ? undefined : steps.get(current.parentId);
33
+ }
34
+ for (const id of path)
35
+ visited.add(id);
36
+ }
37
+ }
38
+ /** Validate declaration identities before exposing references or rendering any content. */
39
+ export function validateKnowledge(knowledge) {
40
+ requireIds("concept", knowledge.concepts ?? []);
41
+ requireIds("workflow", knowledge.workflows ?? []);
42
+ requireIds("article", knowledge.articles ?? []);
43
+ requireIds("dictionary", knowledge.dictionaries ?? []);
44
+ for (const workflow of knowledge.workflows ?? [])
45
+ validateWorkflow(workflow);
46
+ for (const article of knowledge.articles ?? [])
47
+ requireIds(`article '${article.id}'`, article.sections);
48
+ for (const dictionary of knowledge.dictionaries ?? [])
49
+ requireIds(`dictionary '${dictionary.id}'`, dictionary.entries);
50
+ }
51
+ function heading(title, level) {
52
+ return `${"#".repeat(level)} ${title}`;
53
+ }
54
+ function paragraphs(...parts) {
55
+ return parts.filter(part => part !== undefined && part.length > 0).join("\n\n");
56
+ }
57
+ export function conceptToMarkdown(concept) {
58
+ return `${heading(concept.name, 2)}\n${concept.description}`;
59
+ }
60
+ export function sectionToMarkdown(section) {
61
+ return `${heading(section.title, 3)}\n${section.body}`;
62
+ }
63
+ export function articleToMarkdown(article) {
64
+ requireIds(`article '${article.id}'`, article.sections);
65
+ return paragraphs(`${heading(article.title, 2)}\n${article.summary}`, ...article.sections.map(section => section.markdown()));
66
+ }
67
+ export function workflowToMarkdown(workflow) {
68
+ validateWorkflow(workflow);
69
+ const children = new Map();
70
+ for (const step of workflow.steps) {
71
+ const siblings = children.get(step.parentId) ?? [];
72
+ siblings.push(step);
73
+ children.set(step.parentId, siblings);
74
+ }
75
+ const render = (parentId, indent) => {
76
+ return (children.get(parentId) ?? []).map((step, index) => {
77
+ const marker = `${index + 1}. `;
78
+ const childIndent = indent + " ".repeat(marker.length);
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");
83
+ };
84
+ return paragraphs(`${heading(workflow.title, 2)}\n${workflow.summary}`, render(undefined, ""));
85
+ }
86
+ export function stepToMarkdown(step) {
87
+ // Preserve a block boundary before authored descriptions such as indented code and thematic breaks.
88
+ return paragraphs(`**${step.title}**`, step.description);
89
+ }
90
+ export function entryToMarkdown(entry) {
91
+ return paragraphs(`${heading(entry.name, 3)}\n${entry.description}`, entry.aliases?.length ? `Aliases: ${entry.aliases.join(", ")}` : undefined);
92
+ }
93
+ /** Entries own their domain-specific output; the dictionary owns its title, source and order. */
94
+ export function dictionaryToMarkdown(dictionary) {
95
+ requireIds(`dictionary '${dictionary.id}'`, dictionary.entries);
96
+ const source = dictionary.source;
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()));
98
+ }
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
+ validateKnowledge(knowledge);
106
+ const concepts = knowledge.concepts ?? [];
107
+ const workflows = knowledge.workflows ?? [];
108
+ const articles = knowledge.articles ?? [];
109
+ const dictionaries = knowledge.dictionaries ?? [];
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);
111
+ }
package/dist/product.d.ts CHANGED
@@ -1,14 +1,13 @@
1
+ import type { Knowledge } from "./knowledge.js";
1
2
  /** A business identity independent of repository layout.
2
3
  * Consumers own its many-to-many associations with Components.
3
- * @spec Description, concepts and workflows describe product-wide knowledge, not identity or runtime evidence.
4
+ * @spec Description and Knowledge describe the product, independently of identity and runtime evidence.
4
5
  * @see {@link component://docs/kernel.md}
5
6
  */
6
7
  export interface Product {
7
8
  readonly name: string;
9
+ /** Structured offline knowledge; not part of identity or runtime evidence. */
10
+ readonly knowledge?: Knowledge;
8
11
  /** Optional short introduction; not part of Product identity. */
9
12
  readonly description?: string;
10
- /** Optional Markdown: domain objects, relationships and constraints. */
11
- readonly concepts?: string;
12
- /** Optional Markdown: collaboration from trigger to completion; not an executable plan. */
13
- readonly workflows?: string;
14
13
  }
@@ -1,6 +1,15 @@
1
1
  import type { Forge } from "./forge.js";
2
- /** A repository identified by its Forge and path within that Forge. */
2
+ /**
3
+ * A repository identified by its Forge and path within that Forge.
4
+ * @spec forkedFrom is the direct parent path within the same Forge instance, not repository identity.
5
+ * @see {@link component://docs/kernel.md}
6
+ */
3
7
  export interface Repository {
4
8
  readonly forge: Forge;
5
9
  readonly path: string;
10
+ /**
11
+ * Direct parent repository path within this same Forge instance; omission/null is unspecified.
12
+ * Cross-Forge import/mirror provenance belongs in separate metadata.
13
+ */
14
+ readonly forkedFrom?: string | null;
6
15
  }
package/dist/service.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { Knowledge } from "./knowledge.js";
1
2
  import type { Component } from "./component.js";
2
3
  import type { Environment } from "./environment.js";
3
4
  import type { Workload } from "./workload.js";
@@ -5,16 +6,21 @@ import type { Workload } from "./workload.js";
5
6
  * One Component's logical runtime presence in an Environment.
6
7
  * @spec Identity is scoped by name, Component and Environment, not platform resource names.
7
8
  * @rule Workload mappings may change without changing Service identity; declarations prove no live instances.
8
- * @spec Description, concepts and workflows are discovery metadata, not identity or runtime evidence.
9
+ * @spec Description and Knowledge are discovery metadata, independent of identity and runtime evidence.
10
+ * @spec upstream names callers of this Service; omitted is undeclared and an empty list declares no callers.
9
11
  * @see {@link component://docs/kernel.md}
10
12
  * Consumers extend this model with their own capabilities and execution policy.
11
13
  */
12
14
  export interface Service<E extends Environment = Environment> {
13
15
  readonly name: string;
14
- /** Optional Markdown: domain objects, relationships and constraints. */
15
- readonly concepts?: string;
16
- /** Optional Markdown: collaboration from trigger to completion; not an executable plan. */
17
- readonly workflows?: string;
16
+ /** Structured offline knowledge; not part of identity or runtime evidence. */
17
+ readonly knowledge?: Knowledge;
18
+ /**
19
+ * Canonical names of Services that call this Service, resolved in the consumer's catalog.
20
+ * Omitted means undeclared; an empty list declares no upstream callers.
21
+ * This is offline architecture metadata, independent of identity and observed traffic.
22
+ */
23
+ readonly upstream?: readonly string[];
18
24
  /** Human-readable discovery metadata; not part of Service identity. */
19
25
  readonly description?: string;
20
26
  readonly component: Component;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compforge/harness-common",
3
- "version": "0.3.7",
3
+ "version": "0.5.1",
4
4
  "description": "Execution-neutral client lifecycle and datasource contracts for harnesses",
5
5
  "type": "module",
6
6
  "scripts": {