pactwright 0.0.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/LICENSE +201 -0
- package/README.md +68 -0
- package/dist/adapter/claude-code.d.ts +53 -0
- package/dist/adapter/claude-code.js +241 -0
- package/dist/adapter/commands.d.ts +19 -0
- package/dist/adapter/commands.js +162 -0
- package/dist/atomic.d.ts +6 -0
- package/dist/atomic.js +11 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +561 -0
- package/dist/config/config.d.ts +55 -0
- package/dist/config/config.js +199 -0
- package/dist/config/lifecycle.d.ts +34 -0
- package/dist/config/lifecycle.js +81 -0
- package/dist/config/lock.d.ts +43 -0
- package/dist/config/lock.js +141 -0
- package/dist/context.d.ts +59 -0
- package/dist/context.js +111 -0
- package/dist/errors.d.ts +21 -0
- package/dist/errors.js +25 -0
- package/dist/eval/case.d.ts +123 -0
- package/dist/eval/case.js +17 -0
- package/dist/eval/core-suite.d.ts +3 -0
- package/dist/eval/core-suite.js +431 -0
- package/dist/eval/runner.d.ts +75 -0
- package/dist/eval/runner.js +159 -0
- package/dist/eval/sandbox.d.ts +39 -0
- package/dist/eval/sandbox.js +143 -0
- package/dist/extension/manage.d.ts +65 -0
- package/dist/extension/manage.js +372 -0
- package/dist/extension/manifest.d.ts +36 -0
- package/dist/extension/manifest.js +164 -0
- package/dist/extension/resolve.d.ts +77 -0
- package/dist/extension/resolve.js +271 -0
- package/dist/graph/edge-schema.d.ts +55 -0
- package/dist/graph/edge-schema.js +0 -0
- package/dist/graph/edges.d.ts +22 -0
- package/dist/graph/edges.js +63 -0
- package/dist/graph/ids.d.ts +14 -0
- package/dist/graph/ids.js +38 -0
- package/dist/graph/lineage.d.ts +48 -0
- package/dist/graph/lineage.js +226 -0
- package/dist/graph/mutations.d.ts +108 -0
- package/dist/graph/mutations.js +356 -0
- package/dist/graph/nodes.d.ts +46 -0
- package/dist/graph/nodes.js +137 -0
- package/dist/graph/revision.d.ts +50 -0
- package/dist/graph/revision.js +75 -0
- package/dist/graph/schema.d.ts +54 -0
- package/dist/graph/schema.js +90 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +33 -0
- package/dist/init.d.ts +47 -0
- package/dist/init.js +132 -0
- package/dist/lifecycle/engine.d.ts +75 -0
- package/dist/lifecycle/engine.js +146 -0
- package/dist/lifecycle/record.d.ts +18 -0
- package/dist/lifecycle/record.js +157 -0
- package/dist/lifecycle/run.d.ts +62 -0
- package/dist/lifecycle/run.js +167 -0
- package/dist/loader.d.ts +38 -0
- package/dist/loader.js +64 -0
- package/dist/pack/capabilities.d.ts +22 -0
- package/dist/pack/capabilities.js +31 -0
- package/dist/pack/locate.d.ts +22 -0
- package/dist/pack/locate.js +80 -0
- package/dist/pack/manifest.d.ts +34 -0
- package/dist/pack/manifest.js +168 -0
- package/dist/pack/resolve.d.ts +92 -0
- package/dist/pack/resolve.js +238 -0
- package/dist/project.d.ts +22 -0
- package/dist/project.js +37 -0
- package/dist/sync.d.ts +54 -0
- package/dist/sync.js +98 -0
- package/dist/validate.d.ts +23 -0
- package/dist/validate.js +32 -0
- package/dist/validation.d.ts +24 -0
- package/dist/validation.js +83 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +8 -0
- package/dist/yaml.d.ts +12 -0
- package/dist/yaml.js +32 -0
- package/package.json +65 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { CORE_EDGE_SCHEMAS, CORE_EDGE_TYPES, createEdgeSchemaRegistry, } from "../graph/edge-schema.js";
|
|
2
|
+
import { canonicalJson } from "../graph/revision.js";
|
|
3
|
+
import { CORE_NODE_SCHEMAS, CORE_NODE_TYPES, createNodeSchemaRegistry, } from "../graph/schema.js";
|
|
4
|
+
import { isPathSource, locatePackage, satisfiesRange, sha256 } from "../pack/locate.js";
|
|
5
|
+
import { loadExtensionManifest } from "./manifest.js";
|
|
6
|
+
import { runtimeVersion } from "../version.js";
|
|
7
|
+
/**
|
|
8
|
+
* Command namespaces the runtime owns; no extension may register them.
|
|
9
|
+
* Includes commands that arrive in later checkpoints so an extension cannot
|
|
10
|
+
* squat on a future core surface.
|
|
11
|
+
*/
|
|
12
|
+
export const RESERVED_NAMESPACES = [
|
|
13
|
+
"agent-pack",
|
|
14
|
+
"context",
|
|
15
|
+
"eval",
|
|
16
|
+
"extension",
|
|
17
|
+
"github",
|
|
18
|
+
"help",
|
|
19
|
+
"init",
|
|
20
|
+
"lifecycle",
|
|
21
|
+
"sync",
|
|
22
|
+
"upgrade",
|
|
23
|
+
"validate",
|
|
24
|
+
"version",
|
|
25
|
+
];
|
|
26
|
+
function manifestHash(manifest) {
|
|
27
|
+
return sha256(canonicalJson({
|
|
28
|
+
id: manifest.id,
|
|
29
|
+
package: manifest.package,
|
|
30
|
+
version: manifest.version,
|
|
31
|
+
pactwright: manifest.pactwright,
|
|
32
|
+
dependencies: manifest.dependencies,
|
|
33
|
+
node_types: manifest.nodeTypes,
|
|
34
|
+
edge_types: manifest.edgeTypes,
|
|
35
|
+
namespaces: manifest.namespaces,
|
|
36
|
+
agent_capabilities: manifest.agentCapabilities,
|
|
37
|
+
github_profile: manifest.githubProfile,
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
/** Reports every dependency cycle among the resolved manifests once. */
|
|
41
|
+
function cycleProblems(byId) {
|
|
42
|
+
const problems = [];
|
|
43
|
+
const states = new Map();
|
|
44
|
+
const stack = [];
|
|
45
|
+
const visit = (id) => {
|
|
46
|
+
const state = states.get(id);
|
|
47
|
+
if (state === "done")
|
|
48
|
+
return;
|
|
49
|
+
if (state === "visiting") {
|
|
50
|
+
const cycle = [...stack.slice(stack.indexOf(id)), id];
|
|
51
|
+
problems.push({
|
|
52
|
+
code: "extension-dependency-cycle",
|
|
53
|
+
message: `extension dependencies form a cycle: ${cycle.join(" → ")}`,
|
|
54
|
+
});
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
states.set(id, "visiting");
|
|
58
|
+
stack.push(id);
|
|
59
|
+
for (const dep of byId.get(id)?.dependencies ?? []) {
|
|
60
|
+
if (byId.has(dep))
|
|
61
|
+
visit(dep);
|
|
62
|
+
}
|
|
63
|
+
stack.pop();
|
|
64
|
+
states.set(id, "done");
|
|
65
|
+
};
|
|
66
|
+
for (const id of [...byId.keys()].sort())
|
|
67
|
+
visit(id);
|
|
68
|
+
return problems;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Resolves every configured extension (Distribution §§4–5): locate the
|
|
72
|
+
* package, load and validate its manifest, check runtime compatibility,
|
|
73
|
+
* dependency completeness, namespace registration and graph-type ownership.
|
|
74
|
+
* Disabled extensions resolve too — their graph types stay registered so
|
|
75
|
+
* existing records keep their meaning — but only enabled extensions
|
|
76
|
+
* contribute namespaces, capabilities and behaviour. Never throws; returns
|
|
77
|
+
* every problem found in one pass.
|
|
78
|
+
*/
|
|
79
|
+
export function resolveExtensions(options) {
|
|
80
|
+
const { extensions, problems } = resolveExtensionsBestEffort(options);
|
|
81
|
+
if (problems.length > 0)
|
|
82
|
+
return { value: undefined, problems };
|
|
83
|
+
return { value: extensions, problems: [] };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Resolution without the all-or-nothing gate: every extension that could be
|
|
87
|
+
* resolved, alongside every problem found. An entry here may itself be the
|
|
88
|
+
* subject of a reported problem (`incompatible-runtime`, a package mismatch,
|
|
89
|
+
* a duplicate type or a cycle), and an extension that could not be located,
|
|
90
|
+
* whose manifest failed to load, or whose declared id disagrees with its
|
|
91
|
+
* configuration key is absent entirely.
|
|
92
|
+
*
|
|
93
|
+
* Use it only for reporting and for decisions that fail closed elsewhere —
|
|
94
|
+
* never to decide behaviour. `removeExtension` needs it because the command
|
|
95
|
+
* that repairs a broken extension set must not be blocked by that set being
|
|
96
|
+
* broken.
|
|
97
|
+
*/
|
|
98
|
+
export function resolveExtensionsBestEffort(options) {
|
|
99
|
+
const { root, config } = options;
|
|
100
|
+
const runtime = options.runtimeVersion ?? runtimeVersion();
|
|
101
|
+
const problems = [];
|
|
102
|
+
const resolved = [];
|
|
103
|
+
const ids = Object.keys(config.extensions).sort();
|
|
104
|
+
for (const id of ids) {
|
|
105
|
+
const entry = config.extensions[id];
|
|
106
|
+
const located = locatePackage(root, entry.source, "extension");
|
|
107
|
+
if (typeof located !== "string") {
|
|
108
|
+
problems.push({
|
|
109
|
+
...located,
|
|
110
|
+
code: located.code === "pack-not-exported" ? "extension-not-exported" : "extension-not-found",
|
|
111
|
+
});
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const loaded = loadExtensionManifest(located);
|
|
115
|
+
if (loaded.value === undefined) {
|
|
116
|
+
problems.push(...loaded.problems);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const manifest = loaded.value;
|
|
120
|
+
const path = `${located}/extension.yml`;
|
|
121
|
+
if (manifest.id !== id) {
|
|
122
|
+
problems.push({
|
|
123
|
+
code: "extension-id-mismatch",
|
|
124
|
+
message: `config.extensions.${id} resolves a manifest declaring id "${manifest.id}"`,
|
|
125
|
+
path,
|
|
126
|
+
});
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (!isPathSource(entry.source) && manifest.package !== entry.source) {
|
|
130
|
+
problems.push({
|
|
131
|
+
code: "extension-package-mismatch",
|
|
132
|
+
message: `extension "${id}" manifest declares package "${manifest.package}" but config.extensions.${id}.source is "${entry.source}"`,
|
|
133
|
+
path,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (!satisfiesRange(runtime, manifest.pactwright)) {
|
|
137
|
+
problems.push({
|
|
138
|
+
code: "incompatible-runtime",
|
|
139
|
+
message: `extension "${id}@${manifest.version}" requires pactwright ${manifest.pactwright}; this runtime is ${runtime}`,
|
|
140
|
+
path,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
resolved.push({ id, dir: located, config: entry, manifest, hash: manifestHash(manifest) });
|
|
144
|
+
}
|
|
145
|
+
// Dependency completeness: every dependency must be configured, and an
|
|
146
|
+
// enabled extension's dependencies must themselves be enabled — the rule
|
|
147
|
+
// that makes disabling a dependency underneath a dependant a reported
|
|
148
|
+
// problem rather than silent breakage (Distribution §4).
|
|
149
|
+
for (const extension of resolved) {
|
|
150
|
+
for (const dep of extension.manifest.dependencies) {
|
|
151
|
+
const configured = Object.hasOwn(config.extensions, dep) ? config.extensions[dep] : undefined;
|
|
152
|
+
if (configured === undefined) {
|
|
153
|
+
problems.push({
|
|
154
|
+
code: "extension-dependency-missing",
|
|
155
|
+
message: `extension "${extension.id}" requires extension "${dep}", which is not configured`,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
else if (extension.config.enabled && !configured.enabled) {
|
|
159
|
+
problems.push({
|
|
160
|
+
code: "extension-dependency-disabled",
|
|
161
|
+
message: `extension "${extension.id}" is enabled but its dependency "${dep}" is disabled`,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
problems.push(...cycleProblems(new Map(resolved.map((e) => [e.id, e.manifest]))));
|
|
167
|
+
// Namespace registration: enabled extensions only (a disabled extension
|
|
168
|
+
// contributes no behaviour), checked against the runtime's own commands
|
|
169
|
+
// and against every other enabled extension.
|
|
170
|
+
const namespaceOwners = new Map();
|
|
171
|
+
for (const extension of resolved.filter((e) => e.config.enabled)) {
|
|
172
|
+
for (const namespace of extension.manifest.namespaces) {
|
|
173
|
+
if (RESERVED_NAMESPACES.includes(namespace)) {
|
|
174
|
+
problems.push({
|
|
175
|
+
code: "reserved-namespace",
|
|
176
|
+
message: `extension "${extension.id}" registers command namespace "${namespace}", which the runtime reserves`,
|
|
177
|
+
});
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const owner = namespaceOwners.get(namespace);
|
|
181
|
+
if (owner !== undefined) {
|
|
182
|
+
problems.push({
|
|
183
|
+
code: "duplicate-namespace",
|
|
184
|
+
message: `extensions "${owner}" and "${extension.id}" both register command namespace "${namespace}"`,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
namespaceOwners.set(namespace, extension.id);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Graph-type ownership: flat shared namespace; collisions with the core
|
|
193
|
+
// types or another extension are configuration errors, reported here
|
|
194
|
+
// rather than thrown at registry construction.
|
|
195
|
+
const nodeOwners = new Map(CORE_NODE_TYPES.map((t) => [t, "core"]));
|
|
196
|
+
const edgeOwners = new Map(CORE_EDGE_TYPES.map((t) => [t, "core"]));
|
|
197
|
+
for (const extension of resolved) {
|
|
198
|
+
for (const [owners, types, kind] of [
|
|
199
|
+
[nodeOwners, extension.manifest.nodeTypes, "node"],
|
|
200
|
+
[edgeOwners, extension.manifest.edgeTypes, "edge"],
|
|
201
|
+
]) {
|
|
202
|
+
for (const type of types) {
|
|
203
|
+
const owner = owners.get(type);
|
|
204
|
+
if (owner !== undefined) {
|
|
205
|
+
problems.push({
|
|
206
|
+
code: `duplicate-${kind}-type`,
|
|
207
|
+
message: `extension "${extension.id}" registers ${kind} type "${type}", already owned by "${owner}"`,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
owners.set(type, extension.id);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return { extensions: resolved, problems };
|
|
217
|
+
}
|
|
218
|
+
/** Manifests of the enabled extensions, the set that contributes behaviour. */
|
|
219
|
+
export function enabledManifests(extensions) {
|
|
220
|
+
return extensions.filter((e) => e.config.enabled).map((e) => e.manifest);
|
|
221
|
+
}
|
|
222
|
+
/** The lock entries recording exactly this resolved extension set (Distribution §6). */
|
|
223
|
+
export function extensionLockEntries(extensions) {
|
|
224
|
+
const versions = new Map(extensions.map((e) => [e.id, e.manifest.version]));
|
|
225
|
+
return Object.fromEntries([...extensions]
|
|
226
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
227
|
+
.map((extension) => {
|
|
228
|
+
const dependencies = Object.fromEntries([...extension.manifest.dependencies]
|
|
229
|
+
.sort()
|
|
230
|
+
.filter((dep) => versions.has(dep))
|
|
231
|
+
.map((dep) => [dep, versions.get(dep)]));
|
|
232
|
+
return [
|
|
233
|
+
extension.id,
|
|
234
|
+
{
|
|
235
|
+
package: extension.manifest.package,
|
|
236
|
+
version: extension.manifest.version,
|
|
237
|
+
hash: extension.hash,
|
|
238
|
+
...(Object.keys(dependencies).length === 0 ? {} : { dependencies }),
|
|
239
|
+
},
|
|
240
|
+
];
|
|
241
|
+
}));
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Graph schemas contributed by the resolved extensions (enabled or not, so
|
|
245
|
+
* records owned by a disabled extension keep their meaning). The manifest
|
|
246
|
+
* registers type names only; contributed schemas are structural — permissive
|
|
247
|
+
* endpoints, no extra required fields — and owned by the extension id.
|
|
248
|
+
*/
|
|
249
|
+
export function extensionSchemas(extensions) {
|
|
250
|
+
const nodes = [];
|
|
251
|
+
const edges = [];
|
|
252
|
+
for (const extension of [...extensions].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
253
|
+
for (const type of extension.manifest.nodeTypes) {
|
|
254
|
+
nodes.push({ type, requiredFields: [] });
|
|
255
|
+
}
|
|
256
|
+
for (const type of extension.manifest.edgeTypes) {
|
|
257
|
+
edges.push({ type, owner: extension.id, sourceTypes: "any", targetTypes: "any" });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return { nodes, edges };
|
|
261
|
+
}
|
|
262
|
+
/** The core registries extended with every type the resolved extensions register. */
|
|
263
|
+
export function composedRegistries(extensions) {
|
|
264
|
+
if (extensions.length === 0)
|
|
265
|
+
return { nodes: CORE_NODE_SCHEMAS, edges: CORE_EDGE_SCHEMAS };
|
|
266
|
+
const contributed = extensionSchemas(extensions);
|
|
267
|
+
return {
|
|
268
|
+
nodes: createNodeSchemaRegistry([...Object.values(CORE_NODE_SCHEMAS), ...contributed.nodes]),
|
|
269
|
+
edges: createEdgeSchemaRegistry([...Object.values(CORE_EDGE_SCHEMAS), ...contributed.edges]),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type Problem } from "../errors.js";
|
|
2
|
+
import { type Edge } from "./edges.js";
|
|
3
|
+
import type { GraphNode } from "./nodes.js";
|
|
4
|
+
/** Owner id of the core Delivery edge types; extensions use their extension id. */
|
|
5
|
+
export declare const CORE_EDGE_OWNER = "core";
|
|
6
|
+
/** The core Delivery edge types (Delivery Graph §13). */
|
|
7
|
+
export declare const CORE_EDGE_TYPES: readonly ["resolves", "selects", "decomposes", "evidences", "supersedes"];
|
|
8
|
+
export type CoreEdgeType = (typeof CORE_EDGE_TYPES)[number];
|
|
9
|
+
/**
|
|
10
|
+
* An edge type schema in the shared typed-edge registry (Delivery Graph §13,
|
|
11
|
+
* §21). `"any"` endpoints accept every registered node type, which is how the
|
|
12
|
+
* shared `supersedes` relation is reused by extension node types instead of
|
|
13
|
+
* being redeclared (Distribution §7).
|
|
14
|
+
*/
|
|
15
|
+
export interface EdgeSchema {
|
|
16
|
+
readonly type: string;
|
|
17
|
+
/** Who registered and validates this type: `"core"` or an extension id. */
|
|
18
|
+
readonly owner: string;
|
|
19
|
+
readonly sourceTypes: readonly string[] | "any";
|
|
20
|
+
readonly targetTypes: readonly string[] | "any";
|
|
21
|
+
/** Source and target must be nodes of the same type. */
|
|
22
|
+
readonly sameType?: boolean;
|
|
23
|
+
/** No edge may point at its own source, and edges of this type must not form a cycle. */
|
|
24
|
+
readonly acyclic?: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** Edge schemas keyed by edge type. */
|
|
27
|
+
export type EdgeSchemaRegistry = Readonly<Record<string, EdgeSchema>>;
|
|
28
|
+
/**
|
|
29
|
+
* Builds a frozen registry. Throws `duplicate-edge-type` when two schemas
|
|
30
|
+
* claim the same type; later extensions compose registries with
|
|
31
|
+
* `createEdgeSchemaRegistry([...Object.values(CORE_EDGE_SCHEMAS), ...own])`.
|
|
32
|
+
*/
|
|
33
|
+
export declare function createEdgeSchemaRegistry(schemas: readonly EdgeSchema[]): EdgeSchemaRegistry;
|
|
34
|
+
/** Registered edge types, sorted. */
|
|
35
|
+
export declare function edgeTypes(registry: EdgeSchemaRegistry): readonly string[];
|
|
36
|
+
/**
|
|
37
|
+
* The core Delivery relations (Delivery Graph §13):
|
|
38
|
+
*
|
|
39
|
+
* ```text
|
|
40
|
+
* decision --resolves----> intent
|
|
41
|
+
* decision --selects-----> contract
|
|
42
|
+
* brief --decomposes--> contract
|
|
43
|
+
* evidence --evidences---> brief
|
|
44
|
+
* node --supersedes--> same node type
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare const CORE_EDGE_SCHEMAS: EdgeSchemaRegistry;
|
|
48
|
+
/**
|
|
49
|
+
* Validates the shared typed-edge store against the node set and the edge
|
|
50
|
+
* registry (Delivery Graph §21, Edges): declared type, source exists, target
|
|
51
|
+
* exists, valid endpoint types, same-type supersession, no self-supersession,
|
|
52
|
+
* no supersession cycles. Field shape and unique tuples are already enforced
|
|
53
|
+
* by `parseEdges`. `path` is the edges file every problem is reported against.
|
|
54
|
+
*/
|
|
55
|
+
export declare function validateEdges(edges: readonly Edge[], nodes: readonly GraphNode[], registry: EdgeSchemaRegistry, path: string): readonly Problem[];
|
|
Binary file
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Problem } from "../errors.js";
|
|
2
|
+
/** One typed edge in the shared store (Delivery Graph §13). */
|
|
3
|
+
export interface Edge {
|
|
4
|
+
readonly source: string;
|
|
5
|
+
readonly type: string;
|
|
6
|
+
readonly target: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const EDGE_TYPE_PATTERN: RegExp;
|
|
9
|
+
export interface EdgesParseResult {
|
|
10
|
+
/** Successfully parsed edges, in file order. */
|
|
11
|
+
readonly edges: readonly Edge[];
|
|
12
|
+
readonly problems: readonly Problem[];
|
|
13
|
+
}
|
|
14
|
+
export declare function edgeKey(edge: Edge): string;
|
|
15
|
+
/**
|
|
16
|
+
* Parses `specs/graph/edges.yml`: `{ edges: [{source, type, target}, ...] }`.
|
|
17
|
+
* An empty document or `edges: []` is a valid empty store. Duplicate
|
|
18
|
+
* `(source, type, target)` tuples are reported and dropped. Semantic checks
|
|
19
|
+
* (registered type, endpoints, supersession) live in `validateEdges`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseEdges(raw: unknown, path: string): EdgesParseResult;
|
|
22
|
+
export declare function loadEdges(path: string): EdgesParseResult;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Checker, expectRecord, expectString, rejectUnknownKeys, requireKeys, } from "../validation.js";
|
|
2
|
+
import { readYamlFile } from "../yaml.js";
|
|
3
|
+
export const EDGE_TYPE_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
4
|
+
export function edgeKey(edge) {
|
|
5
|
+
return `${edge.source} --${edge.type}--> ${edge.target}`;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Parses `specs/graph/edges.yml`: `{ edges: [{source, type, target}, ...] }`.
|
|
9
|
+
* An empty document or `edges: []` is a valid empty store. Duplicate
|
|
10
|
+
* `(source, type, target)` tuples are reported and dropped. Semantic checks
|
|
11
|
+
* (registered type, endpoints, supersession) live in `validateEdges`.
|
|
12
|
+
*/
|
|
13
|
+
export function parseEdges(raw, path) {
|
|
14
|
+
const c = new Checker(path);
|
|
15
|
+
if (raw === null || raw === undefined)
|
|
16
|
+
return { edges: [], problems: [] };
|
|
17
|
+
const root = expectRecord(c, raw, "edges file");
|
|
18
|
+
if (root === undefined)
|
|
19
|
+
return { edges: [], problems: c.problems };
|
|
20
|
+
requireKeys(c, root, "edges file", ["edges"]);
|
|
21
|
+
rejectUnknownKeys(c, root, "edges file", ["edges"]);
|
|
22
|
+
const list = root["edges"];
|
|
23
|
+
if (list === null || list === undefined)
|
|
24
|
+
return { edges: [], problems: c.problems };
|
|
25
|
+
if (!Array.isArray(list)) {
|
|
26
|
+
c.fail("invalid-type", "edges must be a list");
|
|
27
|
+
return { edges: [], problems: c.problems };
|
|
28
|
+
}
|
|
29
|
+
const edges = [];
|
|
30
|
+
const seen = new Set();
|
|
31
|
+
list.forEach((item, index) => {
|
|
32
|
+
const label = `edges[${index}]`;
|
|
33
|
+
const record = expectRecord(c, item, label);
|
|
34
|
+
if (record === undefined)
|
|
35
|
+
return;
|
|
36
|
+
requireKeys(c, record, label, ["source", "type", "target"]);
|
|
37
|
+
rejectUnknownKeys(c, record, label, ["source", "type", "target"]);
|
|
38
|
+
const source = expectString(c, record["source"], `${label}.source`);
|
|
39
|
+
const type = expectString(c, record["type"], `${label}.type`);
|
|
40
|
+
const target = expectString(c, record["target"], `${label}.target`);
|
|
41
|
+
if (source === undefined || type === undefined || target === undefined)
|
|
42
|
+
return;
|
|
43
|
+
if (!EDGE_TYPE_PATTERN.test(type)) {
|
|
44
|
+
c.fail("invalid-value", `${label}.type "${type}" must be a lowercase kebab-case token`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const edge = { source, type, target };
|
|
48
|
+
const key = edgeKey(edge);
|
|
49
|
+
if (seen.has(key)) {
|
|
50
|
+
c.fail("duplicate-edge", `${label} repeats edge ${key}`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
seen.add(key);
|
|
54
|
+
edges.push(edge);
|
|
55
|
+
});
|
|
56
|
+
return { edges, problems: c.problems };
|
|
57
|
+
}
|
|
58
|
+
export function loadEdges(path) {
|
|
59
|
+
const read = readYamlFile(path);
|
|
60
|
+
if (read.problems.length > 0)
|
|
61
|
+
return { edges: [], problems: read.problems };
|
|
62
|
+
return parseEdges(read.value, path);
|
|
63
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns a title into the `<slug>` part of a node id: lowercase, runs of
|
|
3
|
+
* non-alphanumerics become single dashes, truncated to `MAX_SLUG_LENGTH`.
|
|
4
|
+
* `undefined` when nothing usable remains. Truncation never harms
|
|
5
|
+
* uniqueness: `mintNodeId` hashes the full creation input, not the slug.
|
|
6
|
+
*/
|
|
7
|
+
export declare function slugify(title: string): string | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* Mints a `<type>-<slug>-<short-hash>` node id (Delivery Graph §5). The
|
|
10
|
+
* short-hash is the first 8 hex digits of sha256 over type, slug and seed,
|
|
11
|
+
* so the same creation input always mints the same id; on a collision with
|
|
12
|
+
* an existing id more digits are taken.
|
|
13
|
+
*/
|
|
14
|
+
export declare function mintNodeId(type: string, slug: string, seed: string, taken: ReadonlySet<string>): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* The longest slug a node id may carry. The id adds the type, a dash and up
|
|
4
|
+
* to 64 hash digits, and the filename adds `.md`; 180 keeps the whole name
|
|
5
|
+
* comfortably under the common 255-byte filesystem limit.
|
|
6
|
+
*/
|
|
7
|
+
const MAX_SLUG_LENGTH = 180;
|
|
8
|
+
/**
|
|
9
|
+
* Turns a title into the `<slug>` part of a node id: lowercase, runs of
|
|
10
|
+
* non-alphanumerics become single dashes, truncated to `MAX_SLUG_LENGTH`.
|
|
11
|
+
* `undefined` when nothing usable remains. Truncation never harms
|
|
12
|
+
* uniqueness: `mintNodeId` hashes the full creation input, not the slug.
|
|
13
|
+
*/
|
|
14
|
+
export function slugify(title) {
|
|
15
|
+
const slug = title
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
18
|
+
.replace(/^-+|-+$/g, "")
|
|
19
|
+
.slice(0, MAX_SLUG_LENGTH)
|
|
20
|
+
.replace(/-+$/, "");
|
|
21
|
+
return slug.length === 0 ? undefined : slug;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Mints a `<type>-<slug>-<short-hash>` node id (Delivery Graph §5). The
|
|
25
|
+
* short-hash is the first 8 hex digits of sha256 over type, slug and seed,
|
|
26
|
+
* so the same creation input always mints the same id; on a collision with
|
|
27
|
+
* an existing id more digits are taken.
|
|
28
|
+
*/
|
|
29
|
+
export function mintNodeId(type, slug, seed, taken) {
|
|
30
|
+
const digest = createHash("sha256").update(`${type}\n${slug}\n${seed}`, "utf8").digest("hex");
|
|
31
|
+
for (let length = 8; length <= digest.length; length += 1) {
|
|
32
|
+
const id = `${type}-${slug}-${digest.slice(0, length)}`;
|
|
33
|
+
if (!taken.has(id))
|
|
34
|
+
return id;
|
|
35
|
+
}
|
|
36
|
+
// 64 hex digits colliding means the identical node already exists.
|
|
37
|
+
return `${type}-${slug}-${digest}`;
|
|
38
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Problem } from "../errors.js";
|
|
2
|
+
import type { Edge } from "./edges.js";
|
|
3
|
+
import type { GraphNode } from "./nodes.js";
|
|
4
|
+
/**
|
|
5
|
+
* Derived Delivery lifecycle states (Delivery Graph §14). These are views of
|
|
6
|
+
* canonical graph structure, never stored node fields.
|
|
7
|
+
*/
|
|
8
|
+
export declare const DELIVERY_STATES: readonly ["open", "deferred", "rejected", "contracted", "delivering", "done"];
|
|
9
|
+
export type DeliveryState = (typeof DELIVERY_STATES)[number];
|
|
10
|
+
/**
|
|
11
|
+
* The current Delivery lineage of one intent: the current records that hang
|
|
12
|
+
* off it, one per type, plus the lifecycle state derived from which of them
|
|
13
|
+
* exist. Records that are missing simply are not there yet.
|
|
14
|
+
*/
|
|
15
|
+
export interface Lineage {
|
|
16
|
+
readonly intent: GraphNode;
|
|
17
|
+
readonly decision?: GraphNode;
|
|
18
|
+
readonly contract?: GraphNode;
|
|
19
|
+
readonly brief?: GraphNode;
|
|
20
|
+
readonly evidence?: GraphNode;
|
|
21
|
+
readonly state: DeliveryState;
|
|
22
|
+
/** The intent itself is superseded: the lineage is frozen (§15). */
|
|
23
|
+
readonly superseded: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface LineageResult {
|
|
26
|
+
/** One lineage per unambiguous intent, sorted by intent id. */
|
|
27
|
+
readonly lineages: readonly Lineage[];
|
|
28
|
+
/** Current-lineage ambiguity problems (Delivery Graph §21). */
|
|
29
|
+
readonly problems: readonly Problem[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A record is current when nothing supersedes it (Delivery Graph §15).
|
|
33
|
+
* `isCurrent` for an id nothing points at is `true`; unknown ids are the
|
|
34
|
+
* caller's concern.
|
|
35
|
+
*/
|
|
36
|
+
export declare function isCurrent(id: string, edges: readonly Edge[]): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Derives the current Delivery lineage of every intent from graph structure
|
|
39
|
+
* alone (Delivery Graph §§14–15). Every intent is covered, superseded ones
|
|
40
|
+
* included: a superseded intent's lineage is frozen but must still be
|
|
41
|
+
* unambiguous. Edges with missing or wrongly typed endpoints are ignored,
|
|
42
|
+
* so this is safe to run on a graph `validateEdges` has already rejected.
|
|
43
|
+
*/
|
|
44
|
+
export declare function deriveLineages(nodes: readonly GraphNode[], edges: readonly Edge[]): LineageResult;
|
|
45
|
+
/** The lineage of one intent; `undefined` when the id is not an intent or the lineage is ambiguous. */
|
|
46
|
+
export declare function deriveLineage(intentId: string, nodes: readonly GraphNode[], edges: readonly Edge[]): Lineage | undefined;
|
|
47
|
+
/** Current-lineage ambiguity validation (Delivery Graph §21). */
|
|
48
|
+
export declare function validateLineages(nodes: readonly GraphNode[], edges: readonly Edge[]): readonly Problem[];
|