@compforge/harness-common 0.4.0 → 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,14 +34,20 @@ 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
44
  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." },
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); } },
45
51
  ] }],
46
52
  },
47
53
  upstream: ["gateway"],
@@ -85,8 +91,15 @@ Development: bun install, make lint, make test. Published artifacts are Node-com
85
91
 
86
92
  Product and Service accept an optional `knowledge` declaration. `Knowledge` groups
87
93
  `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.
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.
90
103
 
91
104
  IDs are unique per kind within the owner, and per child collection within its parent.
92
105
  Step `parentId` expresses inclusion; sibling order follows the array even when a parent
@@ -98,12 +111,49 @@ published definitions independently of a containing distribution's version.
98
111
  `knowledge.ts` also provides deterministic, I/O-free Markdown projections:
99
112
  `knowledgeToMarkdown`, `conceptToMarkdown`, `workflowToMarkdown`, `stepToMarkdown`, `articleToMarkdown`,
100
113
  `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.
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.
104
119
 
105
120
  ## Upgrade to 0.4
106
121
 
107
122
  Move the removed Product/Service `concepts` and `workflows` string fields into
108
123
  `knowledge.concepts` and `knowledge.workflows`. Use Concept records for domain objects
109
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.
@@ -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,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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compforge/harness-common",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Execution-neutral client lifecycle and datasource contracts for harnesses",
5
5
  "type": "module",
6
6
  "scripts": {