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.
Files changed (83) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +68 -0
  3. package/dist/adapter/claude-code.d.ts +53 -0
  4. package/dist/adapter/claude-code.js +241 -0
  5. package/dist/adapter/commands.d.ts +19 -0
  6. package/dist/adapter/commands.js +162 -0
  7. package/dist/atomic.d.ts +6 -0
  8. package/dist/atomic.js +11 -0
  9. package/dist/cli.d.ts +2 -0
  10. package/dist/cli.js +561 -0
  11. package/dist/config/config.d.ts +55 -0
  12. package/dist/config/config.js +199 -0
  13. package/dist/config/lifecycle.d.ts +34 -0
  14. package/dist/config/lifecycle.js +81 -0
  15. package/dist/config/lock.d.ts +43 -0
  16. package/dist/config/lock.js +141 -0
  17. package/dist/context.d.ts +59 -0
  18. package/dist/context.js +111 -0
  19. package/dist/errors.d.ts +21 -0
  20. package/dist/errors.js +25 -0
  21. package/dist/eval/case.d.ts +123 -0
  22. package/dist/eval/case.js +17 -0
  23. package/dist/eval/core-suite.d.ts +3 -0
  24. package/dist/eval/core-suite.js +431 -0
  25. package/dist/eval/runner.d.ts +75 -0
  26. package/dist/eval/runner.js +159 -0
  27. package/dist/eval/sandbox.d.ts +39 -0
  28. package/dist/eval/sandbox.js +143 -0
  29. package/dist/extension/manage.d.ts +65 -0
  30. package/dist/extension/manage.js +372 -0
  31. package/dist/extension/manifest.d.ts +36 -0
  32. package/dist/extension/manifest.js +164 -0
  33. package/dist/extension/resolve.d.ts +77 -0
  34. package/dist/extension/resolve.js +271 -0
  35. package/dist/graph/edge-schema.d.ts +55 -0
  36. package/dist/graph/edge-schema.js +0 -0
  37. package/dist/graph/edges.d.ts +22 -0
  38. package/dist/graph/edges.js +63 -0
  39. package/dist/graph/ids.d.ts +14 -0
  40. package/dist/graph/ids.js +38 -0
  41. package/dist/graph/lineage.d.ts +48 -0
  42. package/dist/graph/lineage.js +226 -0
  43. package/dist/graph/mutations.d.ts +108 -0
  44. package/dist/graph/mutations.js +356 -0
  45. package/dist/graph/nodes.d.ts +46 -0
  46. package/dist/graph/nodes.js +137 -0
  47. package/dist/graph/revision.d.ts +50 -0
  48. package/dist/graph/revision.js +75 -0
  49. package/dist/graph/schema.d.ts +54 -0
  50. package/dist/graph/schema.js +90 -0
  51. package/dist/index.d.ts +33 -0
  52. package/dist/index.js +33 -0
  53. package/dist/init.d.ts +47 -0
  54. package/dist/init.js +132 -0
  55. package/dist/lifecycle/engine.d.ts +75 -0
  56. package/dist/lifecycle/engine.js +146 -0
  57. package/dist/lifecycle/record.d.ts +18 -0
  58. package/dist/lifecycle/record.js +157 -0
  59. package/dist/lifecycle/run.d.ts +62 -0
  60. package/dist/lifecycle/run.js +167 -0
  61. package/dist/loader.d.ts +38 -0
  62. package/dist/loader.js +64 -0
  63. package/dist/pack/capabilities.d.ts +22 -0
  64. package/dist/pack/capabilities.js +31 -0
  65. package/dist/pack/locate.d.ts +22 -0
  66. package/dist/pack/locate.js +80 -0
  67. package/dist/pack/manifest.d.ts +34 -0
  68. package/dist/pack/manifest.js +168 -0
  69. package/dist/pack/resolve.d.ts +92 -0
  70. package/dist/pack/resolve.js +238 -0
  71. package/dist/project.d.ts +22 -0
  72. package/dist/project.js +37 -0
  73. package/dist/sync.d.ts +54 -0
  74. package/dist/sync.js +98 -0
  75. package/dist/validate.d.ts +23 -0
  76. package/dist/validate.js +32 -0
  77. package/dist/validation.d.ts +24 -0
  78. package/dist/validation.js +83 -0
  79. package/dist/version.d.ts +2 -0
  80. package/dist/version.js +8 -0
  81. package/dist/yaml.d.ts +12 -0
  82. package/dist/yaml.js +32 -0
  83. 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[];