@intentius/chant 0.32.0 → 0.33.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/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/search.d.ts +82 -0
- package/dist/cli/handlers/search.d.ts.map +1 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +4 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/graph-declared.d.ts +20 -0
- package/dist/graph-declared.d.ts.map +1 -0
- package/dist/graph-effective.d.ts +25 -0
- package/dist/graph-effective.d.ts.map +1 -0
- package/dist/graph-ir.d.ts +7 -0
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/lexicon.d.ts +9 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/observe.d.ts +14 -6
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/dist/observation.d.ts +71 -0
- package/dist/observation.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/handlers/graph.test.ts +1 -1
- package/src/cli/handlers/graph.ts +33 -11
- package/src/cli/handlers/search.test.ts +159 -0
- package/src/cli/handlers/search.ts +314 -0
- package/src/cli/main.ts +7 -0
- package/src/cli/registry.ts +4 -0
- package/src/codegen/release-wiring.test.ts +82 -0
- package/src/config.ts +3 -0
- package/src/graph-declared.ts +33 -0
- package/src/graph-effective.test.ts +97 -0
- package/src/graph-effective.ts +116 -0
- package/src/graph-ir.ts +7 -0
- package/src/lexicon.ts +6 -1
- package/src/lifecycle/observe.test.ts +66 -2
- package/src/lifecycle/observe.ts +79 -18
- package/src/observation.test.ts +135 -0
- package/src/observation.ts +151 -0
package/src/observation.ts
CHANGED
|
@@ -211,3 +211,154 @@ export function formatUnobserved(name: string, entry: UnobservedEntity): string
|
|
|
211
211
|
const base = `${name}${entry.type ? ` (${entry.type})` : ""} — ${unobservedReasonText(entry.reason)}`;
|
|
212
212
|
return entry.detail ? `${base}: ${entry.detail}` : base;
|
|
213
213
|
}
|
|
214
|
+
|
|
215
|
+
/* ------------------------------------------------------------------------- *
|
|
216
|
+
* The observer harness (#1201).
|
|
217
|
+
*
|
|
218
|
+
* Every native observer runs the same control flow: bind to the provider on
|
|
219
|
+
* the applier's own transport, read the declared entities concurrently, and
|
|
220
|
+
* turn each read into one of the tri-state outcomes above. The k8s observer
|
|
221
|
+
* (#1074) and Fly (#767) already embody it. Rather than have aws/gcp/azure each
|
|
222
|
+
* re-derive it — and re-derive it inconsistently, which is how the shell-out
|
|
223
|
+
* observers drifted apart — a lexicon supplies an {@link ObserverAdapter} and
|
|
224
|
+
* the harness owns the shape: bind-or-not-observe-all with a typed reason,
|
|
225
|
+
* bounded concurrency, per-entity tri-state routing, and a per-entity throw
|
|
226
|
+
* degrading to `read-failed` rather than a silent absence.
|
|
227
|
+
*
|
|
228
|
+
* The adapter owns transport and endpoint resolution (an emulator override is
|
|
229
|
+
* resolved inside `bind()` via the shared live-endpoint helper), so the
|
|
230
|
+
* emulator override behaves identically across lexicons by construction — the
|
|
231
|
+
* harness never touches an endpoint itself.
|
|
232
|
+
* ------------------------------------------------------------------------- */
|
|
233
|
+
|
|
234
|
+
/** One declared entity handed to the harness. */
|
|
235
|
+
export interface DeclaredEntity {
|
|
236
|
+
/** chant entity name — the key every outcome is filed under. */
|
|
237
|
+
name: string;
|
|
238
|
+
/** Declared entity type (e.g. `AWS::EC2::VPC`). */
|
|
239
|
+
type: string;
|
|
240
|
+
/** Declared properties, for the adapter to derive a physical address from. */
|
|
241
|
+
props: Record<string, unknown>;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* The outcome of reading one entity, mapped onto the tri-state:
|
|
246
|
+
* - `present` — a key in `resources`.
|
|
247
|
+
* - `absent` — in neither map (the provider was asked and reported it missing).
|
|
248
|
+
* - `unobserved` — a typed NOT-OBSERVED (unsupported kind, filtered, read error).
|
|
249
|
+
*/
|
|
250
|
+
export type EntityObservation =
|
|
251
|
+
| { present: ResourceMetadata }
|
|
252
|
+
| { absent: true }
|
|
253
|
+
| { unobserved: { reason: UnobservedReason; detail?: string } };
|
|
254
|
+
|
|
255
|
+
/** What a lexicon supplies to drive the harness. `Client` is its transport handle. */
|
|
256
|
+
export interface ObserverAdapter<Client> {
|
|
257
|
+
/**
|
|
258
|
+
* Reach the provider on the applier's transport. Throw for a whole-lexicon
|
|
259
|
+
* failure; {@link classifyBindFailure} decides what the throw means.
|
|
260
|
+
*/
|
|
261
|
+
bind(): Promise<Client>;
|
|
262
|
+
/**
|
|
263
|
+
* Map a `bind()` throw to a typed whole-lexicon reason (every entity becomes
|
|
264
|
+
* NOT-OBSERVED with it), or `"rethrow"` for a loud refusal that must not be
|
|
265
|
+
* swallowed — a context/subscription mismatch, which core turns into an
|
|
266
|
+
* honest hole per entity at a higher layer.
|
|
267
|
+
*/
|
|
268
|
+
classifyBindFailure(err: unknown): { reason: UnobservedReason; detail?: string } | "rethrow";
|
|
269
|
+
/** Read one declared entity. A throw here is caught and recorded `read-failed`. */
|
|
270
|
+
read(client: Client, entity: DeclaredEntity): Promise<EntityObservation>;
|
|
271
|
+
/**
|
|
272
|
+
* Run `fn` over `items` concurrently. Supply the transport's own bounded pool
|
|
273
|
+
* (the k8s client's `concurrently`, say); when omitted the harness uses
|
|
274
|
+
* {@link boundedConcurrently}, so "N entities is not N serial spawns" holds
|
|
275
|
+
* for every lexicon whether or not its transport ships a pool.
|
|
276
|
+
*/
|
|
277
|
+
concurrently?<T>(items: readonly T[], fn: (item: T) => Promise<void>): Promise<void>;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Default concurrency for {@link boundedConcurrently} when a transport ships no pool. */
|
|
281
|
+
export const DEFAULT_OBSERVE_CONCURRENCY = 16;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Run `fn` over `items` with at most `limit` in flight. A rejected `fn` rejects
|
|
285
|
+
* the whole run (the harness wraps per-entity reads so this stays for genuinely
|
|
286
|
+
* unexpected faults).
|
|
287
|
+
*/
|
|
288
|
+
export async function boundedConcurrently<T>(
|
|
289
|
+
items: readonly T[],
|
|
290
|
+
fn: (item: T) => Promise<void>,
|
|
291
|
+
limit: number = DEFAULT_OBSERVE_CONCURRENCY,
|
|
292
|
+
): Promise<void> {
|
|
293
|
+
const queue = [...items];
|
|
294
|
+
const size = Math.max(1, Math.min(limit, queue.length || 1));
|
|
295
|
+
const workers = Array.from({ length: size }, async () => {
|
|
296
|
+
for (;;) {
|
|
297
|
+
const next = queue.shift();
|
|
298
|
+
if (next === undefined) return;
|
|
299
|
+
await fn(next);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
await Promise.all(workers);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* The shared observer control flow (#1201). Binds via the adapter, reads every
|
|
307
|
+
* declared entity concurrently, and assembles the tri-state {@link ObservationResult}.
|
|
308
|
+
*
|
|
309
|
+
* A `bind()` throw becomes NOT-OBSERVED for every entity (typed by
|
|
310
|
+
* {@link ObserverAdapter.classifyBindFailure}) unless the adapter asks to
|
|
311
|
+
* rethrow. A per-entity `read()` throw the adapter did not itself map becomes
|
|
312
|
+
* `read-failed` for that one entity — never a silent absence, which would
|
|
313
|
+
* classify as a spurious `create`.
|
|
314
|
+
*/
|
|
315
|
+
export async function observeEntities<Client>(
|
|
316
|
+
declared: readonly DeclaredEntity[],
|
|
317
|
+
adapter: ObserverAdapter<Client>,
|
|
318
|
+
): Promise<ObservationResult> {
|
|
319
|
+
const typesByName: Record<string, string> = {};
|
|
320
|
+
for (const d of declared) typesByName[d.name] = d.type;
|
|
321
|
+
|
|
322
|
+
let client: Client;
|
|
323
|
+
try {
|
|
324
|
+
client = await adapter.bind();
|
|
325
|
+
} catch (err) {
|
|
326
|
+
const verdict = adapter.classifyBindFailure(err);
|
|
327
|
+
if (verdict === "rethrow") throw err;
|
|
328
|
+
return observation(
|
|
329
|
+
{},
|
|
330
|
+
unobservedAll(
|
|
331
|
+
declared.map((d) => d.name),
|
|
332
|
+
verdict.reason,
|
|
333
|
+
verdict.detail,
|
|
334
|
+
typesByName,
|
|
335
|
+
),
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const resources: Record<string, ResourceMetadata> = {};
|
|
340
|
+
const unobserved: Record<string, UnobservedEntity> = {};
|
|
341
|
+
const run = adapter.concurrently ?? ((items, fn) => boundedConcurrently(items, fn));
|
|
342
|
+
|
|
343
|
+
await run(declared, async (entity) => {
|
|
344
|
+
let result: EntityObservation;
|
|
345
|
+
try {
|
|
346
|
+
result = await adapter.read(client, entity);
|
|
347
|
+
} catch (err) {
|
|
348
|
+
result = {
|
|
349
|
+
unobserved: {
|
|
350
|
+
reason: "read-failed",
|
|
351
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
if ("present" in result) {
|
|
356
|
+
resources[entity.name] = result.present;
|
|
357
|
+
} else if ("unobserved" in result) {
|
|
358
|
+
unobserved[entity.name] = { type: entity.type, ...result.unobserved };
|
|
359
|
+
}
|
|
360
|
+
// `absent`: record nothing — in neither map is how the contract spells absence.
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
return observation(resources, unobserved);
|
|
364
|
+
}
|