@mcp-com-ai/hapi 1.0.2-beta.260914 → 1.2.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.
@@ -130,11 +130,11 @@ object shape, and its own `description` states how many entries the array
130
130
  actually needs — read that placeholder's text before replacing it, since the
131
131
  count is stricter than it looks:
132
132
 
133
- - **`test_cases`** — needs **at least 5** entries (the published
134
- `chatgpt-app-submission.v1.json` schema's `minItems: 5`). Prompts a real
135
- user might give that *should* result in one or more of this app's tools
136
- being called, with a plausible expected result. Prefer prompts that
137
- exercise different tools, not five variations of the same one.
133
+ - **`test_cases`** — needs **at least 3 and up to 5** entries (the published
134
+ `chatgpt-app-submission.v1.json` schema's `minItems: 3` and `maxItems: 5` ).
135
+ Prompts a real user might give that *should* result in one or more of this
136
+ app's tools being called, with a plausible expected result. Prefer prompts
137
+ that exercise different tools, not five variations of the same one.
138
138
  - **`negative_test_cases`** — needs **exactly 3** entries. The published
139
139
  schema only declares `minItems: 3` (no maximum), but OpenAI's actual
140
140
  submission form has been observed rejecting a count other than exactly 3
@@ -0,0 +1,452 @@
1
+ ---
2
+ name: hapi-federated-apis
3
+ description: Build a runnable multi-domain HAPI project from two or more valid OpenAPI 3.x contracts, with an Arazzo 1.1 composition, deterministic HAPI Capability Graph planning, local Controller/Service implementations, and repeatable MCP/HTTP demos. Use whenever a user asks to compose, federate, orchestrate, coordinate, or demonstrate multiple APIs/services as one HAPI MCP experience; create Arazzo workflows over several contracts; or build a state-aware multi-step Capability Graph demo. Use this skill even when the request merely says “combine these APIs into HAPI,” “make a multi-service MCP server,” or “create a workflow project.”
4
+ license: MIT
5
+ compatibility: Requires Bun, HAPI CLI with OpenAPI, Arazzo, and Capability Graph plugins, and valid OpenAPI 3.x source contracts. HAPI `init` additionally requires a local writable HAPI home.
6
+ metadata:
7
+ author: mcp.com.ai agent-skills
8
+ version: "1.4.0"
9
+ allowed-tools: Bash(hapi:*) Bash(bun:*) Bash(curl:*) Read Write
10
+ ---
11
+
12
+ # Build a composed HAPI Capability Graph project
13
+
14
+ Use this skill to turn several API domains into a demonstrable HAPI project
15
+ without pretending that a collection of contracts is already a distributed
16
+ runtime graph. The useful outcome is a working local system: source APIs,
17
+ Arazzo workflows, explicit business-state planning, a testable MCP surface,
18
+ and a clear map of what remains future product work.
19
+
20
+ ## Start with the correct mental model
21
+
22
+ There are two different things people call “federation.” Keep them separate:
23
+
24
+ 1. **Composition available now:** several OpenAPI contracts are referenced by
25
+ an Arazzo workflow. Arazzo can execute the resolved workflow steps as one
26
+ MCP tool.
27
+ 2. **One-document graph compatibility available now:** generate a composed
28
+ OpenAPI façade that represents the same operations for the current
29
+ single-document Capability Graph runtime.
30
+ 3. **Runtime graph federation not available by implication:** independent
31
+ graph identities, source conflict resolution, cross-document authorization,
32
+ persistent graph stores, and distributed state are separate work. Never
33
+ market a façade or Arazzo source list as this capability.
34
+
35
+ Call the generated single-document input a **composed façade** or **Plan B**,
36
+ not “graph federation.” Put that statement in the contract and project docs.
37
+
38
+ ## Preconditions
39
+
40
+ Collect these before writing code:
41
+
42
+ - Two or more valid OpenAPI 3.x source paths or URLs. Preserve source language
43
+ and do not overwrite originals unless the user explicitly requests it.
44
+ - A business narrative: actors, desired outcomes, known choices, mutations,
45
+ confirmation needs, and at least one realistic interruption or failure.
46
+ - Whether sources already have usable `servers` URLs. For a local demo, use
47
+ loopback server URLs and synthetic data only.
48
+ - A target project directory. Default to a new sibling project, never alter
49
+ HAPI OSS or Enterprise packages to make a demo work.
50
+
51
+ If source contracts are invalid, stop and report the validator output. Repair
52
+ the source as a distinct task before composing it. Do not invent operations,
53
+ security, or business policy to force a connected graph.
54
+
55
+ ## Choose the project layout
56
+
57
+ Use a layout that makes inputs, generated artifacts, implementation, and
58
+ evidence easy to inspect:
59
+
60
+ ```text
61
+ <project>/
62
+ contracts/
63
+ flights.openapi.yaml # source contract example
64
+ hotels.openapi.yaml
65
+ transfers.openapi.yaml
66
+ trip-composed.openapi.yaml # generated Plan B façade
67
+ arazzo/
68
+ trip.arazzo.yaml # Plan A composition
69
+ specs/
70
+ <project-name>.yaml # local cache for hapi init
71
+ src/<project-name>/
72
+ Controller.ts # generated then completed entry points
73
+ <Domain>Service.ts # business logic
74
+ Store.ts # deterministic demo state when local
75
+ scripts/
76
+ generate-composed.ts
77
+ check-contract-parity.ts
78
+ review.ts
79
+ test/
80
+ docs/
81
+ <project-name>.http
82
+ ```
83
+
84
+ For a self-contained local HAPI project, set `HAPI_HOME` to `<project>`.
85
+ HAPI then uses `<project>/specs`, `<project>/src`, and `<project>/config`,
86
+ instead of a developer's global `~/.hapi` directory.
87
+
88
+ ## Use the installed `hapi` command first
89
+
90
+ All normal setup, generation, review, OpenAPI serving, and Arazzo serving
91
+ commands in this skill use the installed `hapi` binary. Do not make a user run
92
+ `bun packages/cli/src/bin.ts`, depend on a checkout layout, or need HAPI source
93
+ code merely to run their generated project.
94
+
95
+ Before generating the project, verify the installed plugins and flags:
96
+
97
+ ```bash
98
+ hapi --version
99
+ hapi plugins list
100
+ hapi serve --help
101
+ ```
102
+
103
+ Keep regular scripts portable, for example:
104
+
105
+ ```json
106
+ {
107
+ "scripts": {
108
+ "serve": "HAPI_HOME=$PWD hapi serve demo --specs ./contracts/composed.openapi.yaml --mcp --capability-graph --capability-planning --dev --port 3100",
109
+ "serve:workflows": "HAPI_HOME=$PWD hapi arazzo serve ./arazzo/workflows.arazzo.yaml --mcp --port 3101"
110
+ }
111
+ }
112
+ ```
113
+
114
+ An opt-in development script can add `--capability-trace` only when the
115
+ installed `hapi serve --help` advertises it. If it is unavailable, explain that
116
+ an HAPI update is required for traces; do not replace normal project scripts
117
+ with source-tree commands. The offline graph/Arazzo review and ordinary MCP
118
+ demo must still run on the installed command.
119
+
120
+ ## Step 1: inventory the domains before modelling a workflow
121
+
122
+ Read every relevant operation, parameter, schema, response, server, security
123
+ definition, and existing OpenAPI Link. Create a compact inventory:
124
+
125
+ | Domain | Discovery / selection | Mutations | Irreversible or confirmed actions |
126
+ | --- | --- | --- | --- |
127
+ | Flights | list, inspect, select | book, modify | cancel |
128
+ | Hotels | list, inspect, select | book, modify | cancel |
129
+ | Transfers | list, inspect, select | book | cancel |
130
+
131
+ This makes missing seams visible. For example, a workflow cannot safely book a
132
+ transfer that has no source operation or a meaningful verified selection.
133
+
134
+ Treat an empty **selection** cell as a blocker whenever the same row has a
135
+ mutation that needs a choice. Either add the selection operation to the source
136
+ contract (as a separate, explicit repair task) or record the choice as a
137
+ client-verified fact. A domain with a search and a booking but no selection
138
+ compiles cleanly and still cannot be planned.
139
+
140
+ Also list, per domain, each declared response that forces a new decision, such
141
+ as a booking `409` with `unavailable: true`. These are the demo's re-plan
142
+ triggers. Declare each one on the source contract's response with
143
+ `x-hapi-outcome` (for example, produces `hotel.unavailable` and removes
144
+ `hotel.selected`), and make the façade parity check compare outcomes as well
145
+ as `x-hapi-planning`.
146
+
147
+ Keep Controller methods thin. Put deterministic sample inventory, state
148
+ transitions, and failure fixtures in Services. A local demo must not use real
149
+ accounts, payments, customer data, or vendor credentials.
150
+
151
+ ## Step 2: create accurate HCG metadata on source operations
152
+
153
+ Use `x-hapi-capability`, `x-hapi-intent`, `x-hapi-effects`,
154
+ `x-hapi-prerequisites`, and `x-hapi-planning` only when each statement is
155
+ true. The HCG enrichment skill is the specialist companion when the source
156
+ metadata itself needs deeper authoring.
157
+
158
+ ```yaml
159
+ x-hapi-capability:
160
+ - Travel/Flights/Booking
161
+ x-hapi-intent:
162
+ use-when:
163
+ - The traveller has selected a flight and wants to book it.
164
+ avoid-when:
165
+ - The flight is only a search result or the traveller has not approved it.
166
+ x-hapi-effects:
167
+ mode: create # read | create | update | delete | action
168
+ side-effecting: true
169
+ destructive: false
170
+ idempotent: false
171
+ requires-confirmation: true
172
+ x-hapi-prerequisites:
173
+ - Verified traveller identity
174
+ - Explicit flight selection
175
+ x-hapi-planning:
176
+ requires:
177
+ - predicate: traveller.identity.verified
178
+ - predicate: flight.selected
179
+ produces:
180
+ - predicate: flight.booked
181
+ cost: 1
182
+ ```
183
+
184
+ Facts are business state, not commands, unverified fields, or intention:
185
+
186
+ - Good: `traveller.identity.verified`, `flight.selected`, `hotel.booked`.
187
+ - Bad: `bookHotel`, `user.wants.trip`, `date.2026-10-10` before verification.
188
+
189
+ A search can produce `hotels.consulted`; it must not produce
190
+ `hotel.selected` unless the API actually performs that selection. A choice
191
+ made outside the API belongs in the client's verified initial facts. This
192
+ boundary is how a plan tells an MCP client exactly what it must clarify.
193
+
194
+ Use `requires`, `produces`, `removes`, and a finite nonnegative `cost`. A
195
+ cancellation, for example, produces `flight.cancelled` and removes
196
+ `flight.booked`. Do not infer planning transitions from prose, HTTP method,
197
+ classifier output, or a speculative OpenAPI Link.
198
+
199
+ Cross-domain `requires` need a real business rule. `bookHotel` requiring
200
+ `flight.booked` means no traveller can book a hotel alone. Write such rules in
201
+ `docs/FACTS.md` or remove them.
202
+
203
+ Never add an OpenAPI Link that maps a search result into a selection input. A
204
+ selection is the person's decision, and the plan must pause to ask for it.
205
+
206
+ ## Step 3: make Plan A Arazzo composition work first
207
+
208
+ Create an Arazzo 1.1 document with one relative `sourceDescription` per source
209
+ contract. Reference operations by their stable operation IDs and map each
210
+ input, output, and success condition completely.
211
+
212
+ ```yaml
213
+ arazzo: 1.1.0
214
+ info: { title: Trip workflows, version: 1.0.0 }
215
+ sourceDescriptions:
216
+ - name: flights
217
+ url: ../contracts/flights.openapi.yaml
218
+ type: openapi
219
+ - name: hotels
220
+ url: ../contracts/hotels.openapi.yaml
221
+ type: openapi
222
+ workflows:
223
+ - workflowId: bookTrip
224
+ summary: Book a selected flight and hotel
225
+ inputs:
226
+ type: object
227
+ required: [flightId, hotelId, travellerId]
228
+ properties:
229
+ flightId: { type: string }
230
+ hotelId: { type: string }
231
+ travellerId: { type: string }
232
+ steps:
233
+ - stepId: bookFlight
234
+ operationId: $sourceDescriptions.flights.bookFlight
235
+ requestBody:
236
+ contentType: application/json
237
+ payload: { flightId: $inputs.flightId, travellerId: $inputs.travellerId }
238
+ successCriteria: [{ condition: $statusCode == 200 }]
239
+ ```
240
+
241
+ Give a workflow `x-hapi-planning` only when its resolved steps make one stable,
242
+ atomic business transition. Derive its metadata from the steps it calls:
243
+
244
+ - `requires` covers every step operation's requirement that no earlier step
245
+ produces. A `bookTrip` workflow that books a transfer requires
246
+ `transfer.selected`.
247
+ - `produces` includes each domain fact the steps produce, such as
248
+ `flight.booked`, `hotel.booked` and `transfer.booked`, alongside any
249
+ aggregate such as `trip.booked`. Otherwise modify and cancel goals become
250
+ unplannable after the workflow runs.
251
+ - `removes` includes every fact a step removes.
252
+
253
+ Write a parity check that compares each planning-eligible workflow against its
254
+ steps' operation metadata, the same way the façade parity check compares
255
+ operations. A workflow with nested workflows or dynamic
256
+ success/failure branches must not become planning-eligible merely to make a
257
+ diagram look more complete. Failure and compensation are not automatically
258
+ provided by Arazzo or Capability Planning.
259
+
260
+ Validate before continuing:
261
+
262
+ ```bash
263
+ hapi arazzo validate ./arazzo/trip.arazzo.yaml
264
+ ```
265
+
266
+ ## Step 4: generate a checked Plan B composed façade
267
+
268
+ Current Capability Graph compilation consumes one OpenAPI document per runtime.
269
+ Generate one façade from the source contracts; do not hand-maintain a second
270
+ copy. The generator should:
271
+
272
+ 1. Merge paths only after checking globally unique operation IDs.
273
+ 2. Merge or safely namespace component names.
274
+ 3. Preserve each operation's HCG metadata and request/response contract.
275
+ 4. Add only project-owned aggregate routes, such as a synthetic itinerary, in
276
+ a clearly separate generator section.
277
+ 5. Mark `info.x-hapi-demo-composition.not-runtime-federation: true`.
278
+
279
+ Create a parity check that fails if a source operation is missing, duplicated,
280
+ or has different `x-hapi-planning` metadata in the façade. Then compile it:
281
+
282
+ ```bash
283
+ hapi graph compile ./contracts/trip-composed.openapi.yaml --out artifacts/trip.graph.json
284
+ hapi graph inspect artifacts/trip.graph.json
285
+ ```
286
+
287
+ ## Step 5: review state behaviour before running mutations
288
+
289
+ First close the fact vocabulary. Every required fact must be produced by some
290
+ transition or be a documented client-verified fact, and every outcome must be
291
+ reachable. Run the audit on the compiled façade:
292
+
293
+ ```bash
294
+ hapi graph audit artifacts/trip.graph.json
295
+ ```
296
+
297
+ Declare client-established facts such as identity with root-level
298
+ `x-hapi-client-facts` in each source contract that requires them, and have the
299
+ façade generator merge them without duplicates.
300
+
301
+ It exits 1 on an orphan requirement or an unreachable outcome, so wire it into
302
+ the project's review script. When the installed CLI predates `graph audit`,
303
+ probe for it and fall back to the bundled script:
304
+
305
+ ```bash
306
+ bun <skill-dir>/scripts/audit-planning-facts.ts artifacts/trip.graph.json \
307
+ --verified traveller.identity.verified --simulate
308
+ ```
309
+
310
+ `hapi arazzo validate` also warns when a workflow's planning metadata disagrees
311
+ with its steps. Run it with `--strict` in the review script so those warnings
312
+ fail the review. A `clarification-required` result with
313
+ `missing-planning-facts`, `max-depth-exceeded` and zero steps almost always
314
+ means an orphan requirement. Record any intentionally unreachable outcome in
315
+ `docs/FACTS.md` with the reason.
316
+
317
+ Then run four planning cases for each important goal:
318
+
319
+ ```bash
320
+ # Goal is already true.
321
+ hapi graph simulate artifacts/trip.graph.json \
322
+ --initial-fact trip.booked --goal-fact trip.booked
323
+
324
+ # Missing verified choice or confirmation must remain clarification-required.
325
+ hapi graph simulate artifacts/trip.graph.json \
326
+ --initial-fact traveller.identity.verified --goal-fact trip.booked
327
+
328
+ # A permitted, gate-satisfied path is planned deterministically.
329
+ hapi graph simulate artifacts/trip.graph.json \
330
+ --initial-fact traveller.identity.verified \
331
+ --initial-fact flight.selected \
332
+ --initial-fact hotel.selected \
333
+ --goal-fact trip.booked --satisfied-gate mutation-confirmation
334
+ ```
335
+
336
+ Generate a Business State Flow and a goal-specific Plan Trace. Facts are not
337
+ formal UML states: the planner state is a set of facts. The HTML is local,
338
+ read-only inspection output; it never holds credentials, invokes an API, or
339
+ executes a plan.
340
+
341
+ ```bash
342
+ hapi graph visualize artifacts/trip.graph.json \
343
+ --initial-fact traveller.identity.verified \
344
+ --goal-fact trip.booked \
345
+ --out artifacts/trip-state-flow.html
346
+ ```
347
+
348
+ For a presentation, add `--initial-view flow-cards`. The artifact then opens
349
+ on step cards with confirmation waits, required inputs and "Otherwise" lines
350
+ for declared response outcomes, followed by any gap cards. Declared client
351
+ facts are used automatically.
352
+
353
+ ## Step 6: initialize and implement the local HAPI backend
354
+
355
+ Cache the composed façade at `specs/<project-name>.yaml`, then run:
356
+
357
+ ```bash
358
+ HAPI_HOME=$PWD hapi openapi init <project-name>
359
+ ```
360
+
361
+ Retain the generated `src/<project-name>/Controller.ts` as the entry-point
362
+ layer. Complete its methods so they delegate to Services; never replace it
363
+ with a parallel ad-hoc server. Make seed state resettable and deterministic.
364
+
365
+ For an all-local setup, source contracts use the backend's loopback `servers`
366
+ URL. Start the OpenAPI backend without `--url` or `--headless`:
367
+
368
+ ```bash
369
+ HAPI_HOME=$PWD hapi serve <project-name> \
370
+ --specs ./contracts/trip-composed.openapi.yaml \
371
+ --mcp --capability-graph --capability-planning --dev --port 3100
372
+ ```
373
+
374
+ Start the workflow MCP surface separately, usually on port 3101. It uses the
375
+ source contracts' declared loopback server and needs no URL override:
376
+
377
+ ```bash
378
+ HAPI_HOME=$PWD hapi arazzo serve ./arazzo/trip.arazzo.yaml --mcp --port 3101
379
+ ```
380
+
381
+ If the active CLI supports it, use `--capability-trace` only with graph mode
382
+ and development mode. Treat `[HCG_TRACE]` as redacted development evidence,
383
+ not a business audit log. It must not contain raw query text, fact values,
384
+ arguments, responses, credentials, principal data, or confirmations.
385
+
386
+ ## Step 7: demonstrate the client-led loop accurately
387
+
388
+ The portable decision-authority loop is:
389
+
390
+ 1. `capability_context` receives a natural-language request and returns a
391
+ bounded declared vocabulary of candidate goals, relations, gates, and tools.
392
+ 2. The client chooses a declared goal. Context does not establish truth.
393
+ 3. `capability_plan` receives that goal and **only verified** initial facts.
394
+ 4. A `clarification-required` result tells the client what must be verified or
395
+ confirmed next; gather it conversationally or through a client capability.
396
+ 5. The client invokes a permitted direct tool or an Arazzo workflow.
397
+ 6. Observe the result, retain only verified state, and re-plan.
398
+
399
+ In default `decision` authority, HAPI does not call the next tool for the
400
+ client. `capability_plan` is read-only and never grants authorization.
401
+
402
+ Guarded execution is optional and needs `--runtime-authority guarded`, an
403
+ exact plan, current revalidation, and a trusted externally issued confirmation
404
+ receipt for mutations. Do not add a fake in-process verifier just to make a
405
+ demo look autonomous. TypeSafe/Jev classification, when licensed, only ranks
406
+ an already authorized shortlist; it cannot establish business facts, approve,
407
+ or execute a plan.
408
+
409
+ ## Test like a reviewer, not just a developer
410
+
411
+ Create a `.http` collection and a repeatable script. Cover:
412
+
413
+ - `tools/list`, `capability_context`, and `capability_plan` requests.
414
+ - Discovery that produces consultation but not selection.
415
+ - Missing fact / missing confirmation clarification.
416
+ - A valid book, modify, and cancel flow.
417
+ - A deterministic failure (for example unavailable inventory), an explicit new
418
+ user choice, and a re-plan. Assert no fabricated booking fact or automatic
419
+ continuation. Cover every re-plan trigger from the Step 1 inventory.
420
+ - `hapi graph audit` (or the fallback script with `--simulate`) and `hapi arazzo validate`
421
+ without planning warnings, or with each exception documented.
422
+ - Arazzo source validation and one successful multi-step workflow run.
423
+ - Contract parity, graph compile/inspect/simulate/visualize, direct Service
424
+ tests, and trace-redaction checks.
425
+
426
+ Do not put secrets, real customers, payment details, or production inventory
427
+ in examples, logs, fixtures, screenshots, or generated HTML.
428
+
429
+ ## Deliverables and handoff
430
+
431
+ Finish with:
432
+
433
+ 1. Source contract paths, the Arazzo document, and generated façade path.
434
+ 2. Project-local setup/start/stop commands and ports.
435
+ 3. HCG fact vocabulary and the distinction between declared vocabulary and
436
+ verified state.
437
+ 4. Test commands/results, graph fingerprint, and a link to the visualization.
438
+ 5. The fact audit output: client-verified facts, outcomes, and the
439
+ reachability sweep.
440
+ 6. Re-plan triggers: each failure response, its declared `x-hapi-outcome`,
441
+ and the question asked next.
442
+ 7. An explicit current-versus-future matrix covering true federation,
443
+ persistence, elicitation, autonomous cross-turn orchestration,
444
+ compensation, resumable execution, and event or schedule triggers. HCG
445
+ plans toward a goal. Webhooks and callbacks can be described with
446
+ `x-hapi-event`, but HAPI never receives them, and AsyncAPI messages and
447
+ time are not modelled, so never fake those with invented facts.
448
+
449
+ If a user asks only to enrich existing contracts, use the narrower
450
+ `jev-mcp-capability-graph` skill instead. If they want a single API turned into
451
+ an MCP server, use `swagger-to-mcp`. Use this skill when several domains must
452
+ be composed into a runnable project and demo.
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Audit the planning fact vocabulary of a compiled HAPI Capability Graph.
4
+ *
5
+ * Reports:
6
+ * - orphan requirements: facts some transition requires but no transition
7
+ * produces and the author has not declared as client-verified;
8
+ * - outcomes: business facts produced but never required (candidate goals);
9
+ * - operations without a planning transition;
10
+ * - with --simulate, a reachability sweep that runs `hapi graph simulate`
11
+ * for every outcome with all planning operations eligible and all
12
+ * non-authorization gates satisfied. Any non-`planned` result is a gap.
13
+ *
14
+ * Usage:
15
+ * bun audit-planning-facts.ts <graph.json> [--verified <predicate>]... [--simulate]
16
+ *
17
+ * Exit code 1 when an orphan requirement or unreachable outcome is found.
18
+ * Read-only: never calls an upstream API.
19
+ */
20
+
21
+ type Fact = { predicate: string; value?: unknown };
22
+ type Gate = { code: string; kind: string };
23
+ type Transition = {
24
+ id: string;
25
+ requires: Fact[];
26
+ produces: Fact[];
27
+ removes: Fact[];
28
+ gates?: Gate[];
29
+ target: { id: string; kind: string };
30
+ };
31
+ type Graph = {
32
+ operations: Array<{ id: string }>;
33
+ planning?: { transitions: Transition[] };
34
+ };
35
+
36
+ const TECHNICAL = /^operation\..+\.completed$/;
37
+ const factKey = (fact: Fact) =>
38
+ fact.value === undefined ? fact.predicate : `${fact.predicate}=${JSON.stringify(fact.value)}`;
39
+
40
+ function parseArgs(argv: string[]) {
41
+ const verified = new Set<string>();
42
+ let graphPath: string | undefined;
43
+ let simulate = false;
44
+ for (let i = 0; i < argv.length; i++) {
45
+ const arg = argv[i];
46
+ if (arg === "--verified") verified.add(argv[++i] ?? "");
47
+ else if (arg === "--simulate") simulate = true;
48
+ else graphPath ??= arg;
49
+ }
50
+ if (!graphPath) {
51
+ console.error("Usage: bun audit-planning-facts.ts <graph.json> [--verified <predicate>]... [--simulate]");
52
+ process.exit(2);
53
+ }
54
+ return { graphPath, verified, simulate };
55
+ }
56
+
57
+ function indexFacts(transitions: Transition[]) {
58
+ const requiredBy = new Map<string, string[]>();
59
+ const producedBy = new Map<string, string[]>();
60
+ const add = (map: Map<string, string[]>, key: string, id: string) =>
61
+ map.set(key, [...(map.get(key) ?? []), id]);
62
+ for (const t of transitions) {
63
+ for (const f of t.requires) add(requiredBy, factKey(f), t.target.id);
64
+ for (const f of t.produces) if (!TECHNICAL.test(f.predicate)) add(producedBy, factKey(f), t.target.id);
65
+ }
66
+ return { requiredBy, producedBy };
67
+ }
68
+
69
+ /** A consultation nothing requires usually means a missing selection step. */
70
+ function deadEndHint(key: string) {
71
+ return /\.consulted$/.test(key) ? " ⚠ dead-end consultation: is a selection operation missing?" : "";
72
+ }
73
+
74
+ async function simulateOutcome(graphPath: string, goal: string, verified: string[], ops: string[], gates: string[]) {
75
+ const args = ["graph", "simulate", graphPath, "--goal-fact", goal];
76
+ for (const f of verified) args.push("--initial-fact", f);
77
+ for (const o of ops) args.push("--eligible-operation", o);
78
+ for (const g of gates) args.push("--satisfied-gate", g);
79
+ const proc = Bun.spawn(["hapi", ...args], { stdout: "pipe", stderr: "pipe" });
80
+ const out = (await new Response(proc.stdout).text()) + (await new Response(proc.stderr).text());
81
+ await proc.exited;
82
+ const outcome = out.match(/Outcome:\s*(\S+)/)?.[1] ?? "error";
83
+ const diagnostics = out.match(/Diagnostics:\s*(.+)/)?.[1]?.trim() ?? "-";
84
+ return { outcome, diagnostics };
85
+ }
86
+
87
+ async function main() {
88
+ const { graphPath, verified, simulate } = parseArgs(process.argv.slice(2));
89
+ const graph = (await Bun.file(graphPath).json()) as Graph;
90
+ const transitions = graph.planning?.transitions ?? [];
91
+ const { requiredBy, producedBy } = indexFacts(transitions);
92
+ let failed = false;
93
+
94
+ const orphans = [...requiredBy.keys()].filter((k) => !producedBy.has(k) && !verified.has(k)).sort();
95
+ const declaredVerified = [...verified].filter((k) => requiredBy.has(k)).sort();
96
+ const outcomes = [...producedBy.keys()].filter((k) => !requiredBy.has(k)).sort();
97
+ const planned = new Set(transitions.filter((t) => t.target.kind === "operation").map((t) => t.target.id));
98
+ const unplanned = graph.operations.map((o) => o.id).filter((id) => !planned.has(id)).sort();
99
+
100
+ console.log(`Transitions: ${transitions.length}`);
101
+ console.log("\nOrphan requirements (required, never produced, not declared client-verified):");
102
+ if (orphans.length === 0) console.log(" none");
103
+ for (const k of orphans) {
104
+ failed = true;
105
+ console.log(` ✗ ${k} required by ${requiredBy.get(k)!.join(", ")}`);
106
+ }
107
+ console.log("\nDeclared client-verified facts:");
108
+ console.log(declaredVerified.length ? declaredVerified.map((k) => ` • ${k}`).join("\n") : " none");
109
+ console.log("\nOutcomes (produced, never required; candidate goals):");
110
+ console.log(outcomes.length ? outcomes.map((k) => ` • ${k} by ${producedBy.get(k)!.join(", ")}${deadEndHint(k)}`).join("\n") : " none");
111
+ console.log("\nOperations without a planning transition (discovery-only):");
112
+ console.log(unplanned.length ? unplanned.map((id) => ` • ${id}`).join("\n") : " none");
113
+
114
+ if (simulate) {
115
+ const gates = [...new Set(transitions.flatMap((t) => (t.gates ?? []).filter((g) => g.kind !== "authorization").map((g) => g.code)))].sort();
116
+ console.log(`\nReachability sweep from {${declaredVerified.join(", ")}} with gates satisfied: ${gates.join(", ") || "-"}`);
117
+ for (const goal of outcomes) {
118
+ const result = await simulateOutcome(graphPath, goal, declaredVerified, [...planned].sort(), gates);
119
+ const ok = result.outcome === "planned" || result.outcome === "complete";
120
+ if (!ok) failed = true;
121
+ console.log(` ${ok ? "✓" : "✗"} ${goal}: ${result.outcome}${ok ? "" : ` (${result.diagnostics})`}`);
122
+ }
123
+ }
124
+ process.exit(failed ? 1 : 0);
125
+ }
126
+
127
+ await main();