@compforge/harness-common 0.3.7 → 0.4.0

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
@@ -38,8 +38,13 @@ import type { Service } from "@compforge/harness-common";
38
38
 
39
39
  const service: Service = {
40
40
  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.",
41
+ 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
+ ] }],
46
+ },
47
+ upstream: ["gateway"],
43
48
  component: {
44
49
  name: "api",
45
50
  repository: { forge: { name: "github" }, path: "example/app" },
@@ -52,11 +57,14 @@ const service: Service = {
52
57
  };
53
58
  ```
54
59
 
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.
60
+ Service and Product accept an optional description for a short introduction, and structured
61
+ `knowledge` for concepts, workflows, articles and dictionaries. Service knowledge covers its own
62
+ responsibilities, while Product knowledge covers end-to-end flows. Consumers maintain this offline
63
+ metadata; it does not change identity, execute a workflow or prove runtime state.
64
+
65
+ Service `upstream` lists the canonical names of Services that call it, resolved in the consumer's
66
+ catalog. Omission means the relationship is undeclared; `[]` declares no upstream callers.
67
+ This describes architecture; runtime traces determine which calls actually occurred.
60
68
 
61
69
  Environment carries identity and an optional local/SSH Host declaration, not a connection. HostEnvironment
62
70
  describes a target running directly on that host. Concrete accessible environments, such as toolbox's
@@ -72,3 +80,30 @@ Python's corresponding package is sdks/python/common (harness-common). Languages
72
80
  and independently versioned packages; they need not implement every model in lockstep.
73
81
 
74
82
  Development: bun install, make lint, make test. Published artifacts are Node-compatible ESM and declarations.
83
+
84
+ ## Structured knowledge
85
+
86
+ Product and Service accept an optional `knowledge` declaration. `Knowledge` groups
87
+ `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.
90
+
91
+ IDs are unique per kind within the owner, and per child collection within its parent.
92
+ Step `parentId` expresses inclusion; sibling order follows the array even when a parent
93
+ appears after its child. `validateKnowledge` rejects duplicate/empty IDs, unknown parents
94
+ and parent cycles before references are exposed. Dictionary entry IDs are unique, while
95
+ names and domain keys such as error codes may repeat. `source.version` describes the
96
+ published definitions independently of a containing distribution's version.
97
+
98
+ `knowledge.ts` also provides deterministic, I/O-free Markdown projections:
99
+ `knowledgeToMarkdown`, `conceptToMarkdown`, `workflowToMarkdown`, `stepToMarkdown`, `articleToMarkdown`,
100
+ `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.
104
+
105
+ ## Upgrade to 0.4
106
+
107
+ Move the removed Product/Service `concepts` and `workflows` string fields into
108
+ `knowledge.concepts` and `knowledge.workflows`. Use Concept records for domain objects
109
+ and Workflow/Step records for business processes. Knowledge remains optional.
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,86 @@
1
+ /** A named domain object and its relationships or constraints. */
2
+ export interface Concept {
3
+ readonly id: string;
4
+ readonly name: string;
5
+ readonly description: string;
6
+ }
7
+ /** A documented business process; steps are descriptions, not executable actions. */
8
+ export interface Workflow {
9
+ readonly id: string;
10
+ readonly title: string;
11
+ readonly summary: string;
12
+ readonly steps: readonly Step[];
13
+ }
14
+ export interface Step {
15
+ readonly id: string;
16
+ /** Inclusion within this Workflow. Array order determines sibling order. */
17
+ readonly parentId?: string;
18
+ readonly title: string;
19
+ readonly description: string;
20
+ }
21
+ /** A self-contained explanation of a topic. */
22
+ export interface Article {
23
+ readonly id: string;
24
+ readonly title: string;
25
+ readonly summary: string;
26
+ readonly sections: readonly Section[];
27
+ }
28
+ export interface Section {
29
+ readonly id: string;
30
+ readonly title: string;
31
+ /** Markdown authored by the knowledge owner. */
32
+ readonly body: string;
33
+ }
34
+ /** Common reading identity; consumers extend entries with their typed domain fields. */
35
+ export interface Entry {
36
+ readonly id: string;
37
+ readonly name: string;
38
+ readonly description: string;
39
+ readonly aliases?: readonly string[];
40
+ }
41
+ export interface KnowledgeSource {
42
+ readonly name: string;
43
+ /** Definition version, independently of a consumer's distribution version. */
44
+ readonly version: string;
45
+ readonly reference?: string;
46
+ }
47
+ /** Entries retain domain fields. Names and codes need not be unique; IDs must be. */
48
+ export interface Dictionary<T extends Entry = Entry> {
49
+ readonly id: string;
50
+ readonly title: string;
51
+ readonly summary: string;
52
+ readonly source?: KnowledgeSource;
53
+ readonly entries: readonly T[];
54
+ }
55
+ /**
56
+ * @spec Knowledge is owner-scoped offline content, independent of runtime evidence and identity.
57
+ * @rule Top-level IDs are unique per kind; child IDs are unique within their containing object.
58
+ * @see {@link component://docs/kernel.md}
59
+ */
60
+ export interface Knowledge {
61
+ readonly concepts?: readonly Concept[];
62
+ readonly workflows?: readonly Workflow[];
63
+ readonly articles?: readonly Article[];
64
+ readonly dictionaries?: readonly Dictionary[];
65
+ }
66
+ /**
67
+ * @spec Parent IDs express inclusion only; missing parents, self-reference and cycles are invalid.
68
+ * @case id=workflow_hierarchy,desc=`Parent appears after child`,expect=`Sibling array order is preserved`
69
+ */
70
+ export declare function validateWorkflow(workflow: Workflow): void;
71
+ /** Validate declaration identities before exposing references or rendering any content. */
72
+ export declare function validateKnowledge(knowledge: Knowledge): void;
73
+ export declare function conceptToMarkdown(concept: Concept): string;
74
+ export declare function sectionToMarkdown(section: Section): string;
75
+ export declare function articleToMarkdown(article: Article): string;
76
+ export declare function stepToMarkdown(step: Step): string;
77
+ export declare function workflowToMarkdown(workflow: Workflow): string;
78
+ 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;
@@ -0,0 +1,116 @@
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 paragraphs(heading(concept.name, 1), concept.description);
59
+ }
60
+ export function sectionToMarkdown(section) {
61
+ return paragraphs(heading(section.title, 2), section.body);
62
+ }
63
+ export function articleToMarkdown(article) {
64
+ requireIds(`article '${article.id}'`, article.sections);
65
+ return paragraphs(heading(article.title, 1), article.summary, ...article.sections.map(sectionToMarkdown));
66
+ }
67
+ function workflowMarkdown(workflow, level) {
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.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");
82
+ };
83
+ return paragraphs(heading(workflow.title, level), workflow.summary, render(undefined, ""));
84
+ }
85
+ export function stepToMarkdown(step) {
86
+ return paragraphs(heading(step.title, 1), step.description);
87
+ }
88
+ export function workflowToMarkdown(workflow) {
89
+ return workflowMarkdown(workflow, 1);
90
+ }
91
+ export function entryToMarkdown(entry) {
92
+ return paragraphs(heading(entry.name, 2), entry.description, entry.aliases?.length ? `Aliases: ${entry.aliases.join(", ")}` : undefined);
93
+ }
94
+ /** Domain renderers can include fields beyond the common entry reading contract. */
95
+ export function dictionaryToMarkdown(dictionary, renderEntry = entryToMarkdown) {
96
+ requireIds(`dictionary '${dictionary.id}'`, dictionary.entries);
97
+ 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)));
99
+ }
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 = {}) {
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 => 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);
116
+ }
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
  }
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.4.0",
4
4
  "description": "Execution-neutral client lifecycle and datasource contracts for harnesses",
5
5
  "type": "module",
6
6
  "scripts": {