algocline-app 0.38.2

algocline application layer — execution orchestration, package management
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
--- alc_shapes — SSoT for the result shape convention.
---
--- Usage:
---   local S = require("alc_shapes")
---   local ok, reason = S.check(value, S.voted)
---   local x = S.assert(value, "voted", "where")
---
--- Core concept: Schema-as-Data (after Malli's TypeSchemaAsData doctrine).
--- Schemas are plain kind-tagged Lua tables. They are the single AST used
--- by every consumer (validator / reflector / codegen / docs projector)
--- and are persistable without loss — JSON-encode, JSON-decode, still
--- valid. Metatables carry combinator sugar only; `rawget` is the
--- universal reader. Parallel representations (e.g. a separate "TypeExpr"
--- AST) are prohibited: downstream consumers must project, not mirror.
--- See README.md §Core concept.
---
--- NOTE: bundled packages declare their I/O contract under
--- `M.spec.entries.run.{input, result}`. Each field accepts either a
--- string (registry lookup key) or an inline `T.shape(...)` schema.
--- Packages with multiple entry points (e.g. calibrate.run vs
--- calibrate.assess) add further `M.spec.entries.{name}` blocks.
--- Packages without `M.spec` are treated as opaque by spec_resolver.
--- Producers self-decorate at module tail with `M.run = S.instrument(
--- M, "run")`; the wrapper reads `M.spec.entries.<entry>.{input,
--- result}` and asserts on each call when `ALC_SHAPE_CHECK=1`.
--- See README.md §Producer usage and §Producer-wrap vs caller-wrap.

local T = require("alc_shapes.t")
local check = require("alc_shapes.check")
local reflect = require("alc_shapes.reflect")
local luacats = require("alc_shapes.luacats")
local spec_resolver = require("alc_shapes.spec_resolver")
local instrument = require("alc_shapes.instrument")

local M = {}

M.VERSION = "0.25.1"

-- ── shape dictionary ─────────────────────────────────────────────────

M.voted = T.shape({
    consensus       = T.string:describe("LLM-synthesized majority summary"),
    answer          = T.string:is_optional():describe("Majority answer (nil when no paths converge)"),
    answer_norm     = T.string:is_optional():describe("Normalized vote key"),
    paths           = T.array_of(T.shape({
        reasoning = T.string,
        answer    = T.string,
    })):describe("Per-path reasoning + extracted answer"),
    votes           = T.array_of(T.string):describe("Normalized vote per path, 1-indexed"),
    vote_counts     = T.map_of(T.string, T.number):describe("{ [norm] = count } tally"),
    n_sampled       = T.number:describe("Number of sampled paths"),
    total_llm_calls = T.number,
}, { open = true })

M.paneled = T.shape({
    arguments = T.array_of(T.shape({
        role = T.string,
        text = T.string,
    })):describe("Per-role position statements"),
    synthesis = T.string:describe("Moderator synthesis"),
}, { open = true })

M.assessed = T.shape({
    answer          = T.string,
    confidence      = T.number:describe("Self-assessed confidence 0.0–1.0"),
    total_llm_calls = T.number,
}, { open = true })

M.calibrated = T.shape({
    answer          = T.string,
    confidence      = T.number:describe("Initial self-assessed confidence"),
    escalated       = T.boolean:describe("Whether fallback was triggered"),
    strategy        = T.one_of({ "direct", "retry", "panel", "ensemble" }),
    total_llm_calls = T.number,
    fallback_detail = T.table:is_optional():describe("Fallback strategy result (voted/paneled)"),
}, { open = true })

-- conformal_vote (Wang et al. 2026, arXiv:2604.07667):
-- linear opinion pool + split conformal prediction with the three-way
-- decision rule (Proposition 3). `card_id` is populated only when the
-- caller opts into Card emission via ctx.auto_card = true.
M.conformal_decided = T.shape({
    action         = T.one_of({ "commit", "escalate", "anomaly" })
        :describe("Three-way decision per Proposition 3"),
    selected       = T.string:is_optional()
        :describe("Committed label (nil when action != 'commit')"),
    prediction_set = T.array_of(T.string)
        :describe("Labels y with P_social(y|x) >= tau"),
    p_social       = T.map_of(T.string, T.number)
        :describe("Linear opinion pool output { [label] = prob }"),
    coverage_level = T.number:describe("1 - alpha (finite-sample guarantee)"),
    q_hat          = T.number:describe("Calibration quantile of nonconformity scores"),
    tau            = T.number:describe("1 - q_hat (prediction-set threshold)"),
    card_id        = T.string:is_optional()
        :describe("Emitted Card id (only when auto_card=true)"),
}, { open = true })

-- dci (Prakash 2026, arXiv:2603.11781):
-- Deliberative Collective Intelligence (DCI-CF). 4 roles × 14 typed
-- epistemic acts × 8-stage convergence, emitting a decision_packet with
-- 5 non-nil components (selected_option / residual_objections /
-- minority_report / next_actions / reopen_triggers). `card_id` is
-- populated only when the caller opts into Card emission via
-- ctx.auto_card = true.
M.deliberated = T.shape({
    answer          = T.string:describe("Selected option's final answer text"),
    decision_packet = T.shape({
        selected_option     = T.shape({
            answer    = T.string,
            rationale = T.string,
            evidence  = T.array_of(T.string),
        }, { open = true }):describe("Chosen option with rationale and cited evidence"),
        residual_objections = T.array_of(T.string)
            :describe("Objections not fully resolved (empty array allowed, nil禁止)"),
        minority_report     = T.array_of(T.shape({
            position   = T.string,
            rationale  = T.string,
            confidence = T.number,
        }, { open = true }))
            :describe("Dissenting positions with confidence (empty array allowed, nil禁止)"),
        next_actions        = T.array_of(T.string)
            :describe("Concrete follow-up actions (empty array allowed, nil禁止)"),
        reopen_triggers     = T.array_of(T.string)
            :describe("Conditions to reopen deliberation (empty array allowed, nil禁止)"),
    }, { open = true }):describe("5-component decision packet; all 5 fields MUST be non-nil"),
    workspace       = T.shape({
        problem_view          = T.string,
        key_frames            = T.array_of(T.string),
        emerging_ideas        = T.array_of(T.string),
        tensions              = T.array_of(T.string),
        synthesis_in_progress = T.string,
        next_actions          = T.array_of(T.string),
    }, { open = true }):describe("Shared workspace 6 fields after finalization"),
    history         = T.array_of(T.table):describe("Per-stage typed-act log (14-act typed)"),
    convergence     = T.one_of({ "dominance", "no_blocking", "fallback" })
        :describe("How the session converged"),
    stats           = T.shape({
        rounds_used     = T.number,
        total_acts      = T.number,
        options_count   = T.number,
        total_llm_calls = T.number,
    }, { open = true }):describe("Execution statistics"),
    card_id         = T.string:is_optional()
        :describe("Emitted Card id (only when auto_card=true)"),
}, { open = true })

-- smc_sample (Markovic-Voronov et al. 2026, arXiv:2604.16453):
-- Block-level Sequential Monte Carlo sampling (Target I specialization).
-- Reward-weighted importance sampling with ESS-triggered multinomial
-- resampling and Metropolis-Hastings rejuvenation. 1 particle = 1
-- complete answer, ψ_t = exp(α · r(answer)) driven by a caller-injected
-- reward_fn. `card_id` is populated only when the caller opts into
-- Card emission via ctx.auto_card = true.
M.smc_sampled = T.shape({
    answer         = T.string:describe("Argmax-weight particle's answer text"),
    particles      = T.array_of(T.shape({
        answer  = T.string:describe("Particle answer text"),
        weight  = T.number:describe("Final normalized importance weight"),
        reward  = T.number:describe("Final reward under caller's reward_fn"),
        history = T.array_of(T.table):describe("Per-iteration trace entries (1 slot in v1)"),
    }, { open = true }))
        :describe("All N particles in their final state"),
    weights        = T.array_of(T.number)
        :describe("Final normalized weights (Σ ≈ 1)"),
    iterations     = T.number:describe("K SMC rounds actually executed"),
    resample_count = T.number:describe("Number of iterations that triggered multinomial resample"),
    ess_trace      = T.array_of(T.number)
        :describe("ESS recorded at the start of each iteration (length K)"),
    stats          = T.shape({
        total_llm_calls    = T.number:describe("alc.llm invocations issued by the pkg"),
        total_reward_calls = T.number:describe("reward_fn invocations (caller-provided)"),
    }, { open = true }):describe("Execution counters (open for diagnostics like mh_rejected)"),
    card_id        = T.string:is_optional()
        :describe("Emitted Card id (only when auto_card=true)"),
}, { open = true })

-- ── ranking shapes ───────────────────────────────────────────────────

local ranked_item_3 = T.shape({
    rank  = T.number,
    index = T.number,
    text  = T.string,
})

local ranked_item_4 = T.shape({
    rank  = T.number,
    index = T.number,
    score = T.number,
    text  = T.string,
})

M.tournament = T.shape({
    best        = T.string:describe("Winner text"),
    best_index  = T.number:describe("Winner original index (1-based)"),
    total_wins  = T.number:describe("Winner's win count"),
    candidates  = T.array_of(T.string):describe("Input candidate texts"),
    matches     = T.array_of(T.shape({
        a      = T.number:describe("Index of first candidate"),
        b      = T.number:describe("Index of second candidate"),
        winner = T.number:describe("Index of the winner"),
        reason = T.string:describe("Judge verdict explanation"),
    })):describe("Pairwise match log"),
}, { open = true })

M.listwise_ranked = T.shape({
    ranked      = T.array_of(ranked_item_3):describe("Full ranking"),
    top_k       = T.array_of(ranked_item_3):describe("Top-k subset"),
    killed      = T.array_of(ranked_item_3):describe("Eliminated candidates"),
    best        = T.string:describe("Top-ranked text"),
    best_index  = T.number:describe("Top-ranked original index (1-based)"),
    n_candidates    = T.number,
    total_llm_calls = T.number,
}, { open = true })

M.pairwise_ranked = T.shape({
    ranked      = T.array_of(ranked_item_4):describe("Full ranking with scores"),
    top_k       = T.array_of(ranked_item_4):describe("Top-k subset"),
    killed      = T.array_of(ranked_item_4):describe("Eliminated candidates"),
    best        = T.string:describe("Top-ranked text"),
    best_index  = T.number:describe("Top-ranked original index (1-based)"),
    method          = T.one_of({ "allpair", "sorting" }):describe("Comparison strategy"),
    score_semantics = T.one_of({ "copeland", "rank_inverse" }):describe("Score interpretation"),
    n_candidates        = T.number,
    total_llm_calls     = T.number,
    position_bias_splits = T.number:describe("Position-bias correction splits"),
    both_tie_pairs       = T.number:describe("Pairs that tied in both directions"),
}, { open = true })

-- ── recipe shapes ────────────────────────────────────────────────────

local funnel_stage = T.discriminated("name", {
    listwise_rank = T.shape({
        name         = T.one_of({ "listwise_rank" }),
        input_count  = T.number,
        output_count = T.number,
        llm_calls    = T.number,
        window_size  = T.number,
    }),
    listwise_skipped = T.shape({
        name         = T.one_of({ "listwise_skipped" }),
        input_count  = T.number,
        output_count = T.number,
        llm_calls    = T.number,
        reason       = T.string,
    }),
    multi_axis_scoring = T.shape({
        name           = T.one_of({ "multi_axis_scoring" }),
        input_count    = T.number,
        output_count   = T.number,
        llm_calls      = T.number,
        axes_count     = T.number,
        parse_failures = T.number,
        score_range    = T.shape({ min = T.number, max = T.number }),
    }),
    scoring_skipped = T.shape({
        name         = T.one_of({ "scoring_skipped" }),
        input_count  = T.number,
        output_count = T.number,
        llm_calls    = T.number,
        reason       = T.string,
    }),
    pairwise_rank_allpair = T.shape({
        name                 = T.one_of({ "pairwise_rank_allpair" }),
        input_count          = T.number,
        output_count         = T.number,
        llm_calls            = T.number,
        position_bias_splits = T.number,
        both_tie_pairs       = T.number,
    }),
    direct_pairwise = T.shape({
        name                 = T.one_of({ "direct_pairwise" }),
        input_count          = T.number,
        output_count         = T.number,
        llm_calls            = T.number,
        position_bias_splits = T.number,
        both_tie_pairs       = T.number,
    }),
})

M.funnel_ranked = T.shape({
    ranking     = T.array_of(T.shape({
        rank           = T.number,
        text           = T.string,
        original_index = T.number:describe("Pre-funnel candidate index (1-based)"),
        pairwise_score = T.number:describe("Copeland score from pairwise stage"),
    })):describe("Final ranking"),
    best            = T.string:describe("Top-ranked text"),
    best_index      = T.number:describe("Top-ranked original index (1-based)"),
    funnel_bypassed = T.boolean:describe("True when N < 6 bypasses funnel stages"),
    bypass_reason   = T.string:is_optional():describe("Reason for bypass (nil when not bypassed)"),
    total_llm_calls     = T.number,
    naive_baseline_calls = T.number:describe("Hypothetical full-pairwise call count"),
    naive_baseline_kind  = T.string:describe("Baseline method identifier"),
    savings_percent = T.number:is_optional():describe("LLM call savings vs baseline (nil on bypass)"),
    warnings        = T.array_of(T.shape({
        code     = T.string:describe("Machine-readable warning identifier"),
        severity = T.one_of({ "warn", "critical" }),
        data     = T.table:describe("Diagnostic payload (structure varies by code)"),
        message  = T.string:describe("Human-readable summary"),
    })):describe("Diagnostic warnings"),
    stages       = T.array_of(funnel_stage):describe("Per-stage detail (discriminated by name)"),
    funnel_shape = T.array_of(T.number):describe("Candidate counts per stage [N, s1_out, s2_out]"),
}, { open = true })

local safe_panel_stage = T.discriminated("name", {
    condorcet = T.shape({
        name              = T.one_of({ "condorcet" }),
        p_estimate        = T.number,
        recommended_n     = T.number,
        expected_accuracy = T.number,
        target_accuracy   = T.number,
        target_met        = T.boolean,
    }),
    condorcet_anti_jury = T.shape({
        name         = T.one_of({ "condorcet_anti_jury" }),
        p_estimate   = T.number,
        is_anti_jury = T.boolean,
    }),
    condorcet_coin_flip = T.shape({
        name         = T.one_of({ "condorcet_coin_flip" }),
        p_estimate   = T.number,
        is_anti_jury = T.boolean,
    }),
    sc = T.shape({
        name               = T.one_of({ "sc" }),
        panel_size         = T.number,
        answer             = T.string,
        plurality_fraction = T.number,
        margin_gap         = T.number,
        n_distinct_answers = T.number,
        unanimous          = T.boolean,
        llm_calls          = T.number,
    }),
    vote_prefix_stability = T.shape({
        name              = T.one_of({ "vote_prefix_stability" }),
        signal_type       = T.string,
        series_length     = T.number,
        is_safe           = T.boolean,
        peak_idx          = T.number,
        consecutive_drops = T.number,
    }),
    vote_prefix_stability_skipped = T.shape({
        name          = T.one_of({ "vote_prefix_stability_skipped" }),
        signal_type   = T.string,
        reason        = T.string,
        series_length = T.number,
        is_safe       = T.boolean,
    }),
    calibrate = T.shape({
        name                = T.one_of({ "calibrate" }),
        confidence          = T.number,
        threshold           = T.number,
        needs_investigation = T.boolean,
        escalated           = T.boolean,
        llm_calls           = T.number,
    }),
})

M.safe_paneled = T.shape({
    answer       = T.string:is_optional():describe("Consensus answer (nil on abort)"),
    confidence   = T.number:describe("Meta-confidence estimate"),
    panel_size   = T.number:describe("Actual panel size used"),
    plurality_fraction = T.number:describe("Top-answer vote fraction"),
    margin_gap   = T.number:describe("(top - runner_up) / n"),
    vote_counts  = T.map_of(T.string, T.number):describe("{ [normalized_answer] = count } tally"),
    n_distinct_answers = T.number:describe("Count of unique answers"),
    expected_accuracy  = T.number:describe("Condorcet expected majority accuracy"),
    target_met         = T.boolean:describe("Whether expected accuracy >= target"),
    is_safe            = T.boolean:describe("Vote-prefix stability safe flag"),
    anti_jury          = T.boolean:describe("Condorcet anti-jury detection"),
    aborted            = T.boolean:describe("True if early-abort triggered"),
    needs_investigation = T.boolean:describe("True if meta-confidence below threshold"),
    unanimous          = T.boolean:describe("All votes identical"),
    total_llm_calls    = T.number,
    abort_reason       = T.string:is_optional():describe("Abort reason (nil when not aborted)"),
    stages             = T.array_of(safe_panel_stage):describe("Per-stage detail (discriminated by name)"),
}, { open = true })

-- ── recipe_quick_vote shape ──────────────────────────────────────────
-- Adaptive-stop majority vote with SPRT gate. Three outcome branches
-- ("confirmed" / "rejected" / "truncated") share the same result shape
-- so consumers can read any field without branching on `outcome`.
local quick_vote_sample = T.shape({
    reasoning = T.string,
    answer    = T.string,
    norm      = T.string,
}, { open = true })

local quick_vote_sprt = T.shape({
    log_lr  = T.number,
    n       = T.number,
    a_bound = T.number,
    b_bound = T.number,
}, { open = true })

local quick_vote_params = T.shape({
    p0    = T.number,
    p1    = T.number,
    alpha = T.number,
    beta  = T.number,
    min_n = T.number,
    max_n = T.number,
}, { open = true })

M.quick_voted = T.shape({
    answer      = T.string:describe("Leader answer from sample 1 (cleaned, not normalized)"),
    leader_norm = T.string:describe("Normalized leader key used for agreement tests"),
    outcome     = T.one_of({ "confirmed", "rejected", "truncated" })
        :describe("Terminal state: confirmed=H1 accepted, rejected=H0 accepted, truncated=no verdict at max_n"),
    verdict     = T.one_of({ "accept_h1", "accept_h0", "continue" })
        :describe("Underlying SPRT verdict from the final decide()"),
    n_samples   = T.number:describe("Total samples drawn (1 leader + k agreement observations)"),
    vote_counts = T.map_of(T.string, T.number):describe("{ [norm] = count } tally across all samples"),
    samples     = T.array_of(quick_vote_sample):describe("Per-sample reasoning + extracted answer"),
    sprt        = quick_vote_sprt:describe("Final SPRT state snapshot"),
    params      = quick_vote_params:describe("Echoed parameter values"),
    total_llm_calls     = T.number:describe("2 × n_samples (reasoning + extract per sample)"),
    needs_investigation = T.boolean:describe("True only when outcome == 'truncated' (evidence inconclusive at declared α/β). 'rejected' is a conclusive verdict and does NOT set this flag."),
}, { open = true })

-- ── recipe_deep_panel shape ──────────────────────────────────────────
-- Stages are heterogeneous per invocation (Stage 1 may be the abort
-- branch; Stage 3's decomp is nullable; Stage 5 always present on
-- main path). Keep the per-stage sub-shape open-ended as T.table so
-- the recipe can evolve per-stage fields without breaking consumers
-- that don't inspect them. open = true at the top level allows
-- forward-compat additions such as future Stage-specific diagnostics.
M.deep_paneled = T.shape({
    answer       = T.any:describe("Plurality answer (nil on abort)"),
    confidence   = T.number:describe("Meta-confidence estimate"),
    panel_size   = T.number:describe("Requested panel size"),
    n_branches_completed = T.number:describe("Branches actually finished"),
    plurality_fraction   = T.number:describe("Top-answer vote fraction"),
    margin_gap           = T.number:describe("(top - runner_up) / n"),
    vote_counts          = T.map_of(T.string, T.number):describe("{ [normalized_answer] = count } tally"),
    n_distinct_answers   = T.number:describe("Count of unique normalized answers"),
    branches             = T.table:describe("{ [bkey] = { approach, answer, best_score, tree_stats } }"),
    expected_accuracy    = T.number:describe("Condorcet expected majority accuracy"),
    target_met           = T.boolean:describe("Whether expected accuracy >= ctx.target_accuracy"),
    anti_jury            = T.boolean:describe("Condorcet anti-jury detection at Stage 1"),
    aborted              = T.boolean:describe("True if early-abort triggered"),
    needs_investigation  = T.boolean:describe("True if meta-confidence below threshold"),
    unanimous            = T.boolean:describe("All normalized votes identical"),
    diversity            = T.table:is_optional():describe("{ n_distinct, distinctness, decomp_status }"),
    decomp               = T.table:is_optional():describe("ensemble_div.decompose output (nil if Stage 3b skipped)"),
    total_llm_calls      = T.number,
    abort_reason         = T.string:is_optional():describe("Abort reason (nil when not aborted)"),
    stages               = T.array_of(T.table):describe("Per-stage detail (heterogeneous)"),
}, { open = true })

-- ── public API re-export ─────────────────────────────────────────────
M.check        = check.check
M.assert       = check.assert
M.assert_dev   = check.assert_dev
M.is_dev_mode  = check.is_dev_mode
M.fields       = reflect.fields
M.walk         = reflect.walk
M.is_schema    = T._internal.is_schema

-- Combinator namespace (so callers can write `S.T.string` without a
-- separate require).
M.T = T

-- Codegen namespace (used by scripts/gen_shapes_luacats.lua).
M.LuaCats = luacats

-- Spec resolver namespace (routing / recipe layer uses this to treat
-- typed bundled pkgs and opaque external pkgs uniformly).
M.spec_resolver = spec_resolver

-- Malli-style producer-wrap instrumentation (see alc_shapes/instrument.lua).
-- Bundled pkgs self-decorate with `M.run = S.instrument(M, "run")` at
-- module tail, reading shapes from `M.spec.entries[entry_name]`.
M.instrument   = instrument.instrument

-- ── reserved-name guard ──────────────────────────────────────────────
-- Certain names collide with `check.assert` shortcut semantics:
--   `M.assert(v, "any")` is always a no-op pass-through. Registering a
-- shape under such a name would silently shadow the shortcut (check.lua).
-- tableshape / Zod avoid this by namespace-separating built-ins from user
-- schemas; we enforce the same invariant via a load-time loud-fail.
-- Re-exported functions / combinator namespaces (M.T, M.LuaCats, etc.)
-- are not shape-kind and therefore never trip this check.
local RESERVED_SHAPE_NAMES = {
    "any", "check", "assert", "assert_dev", "is_dev_mode",
    "fields", "walk", "is_schema", "T", "LuaCats", "spec_resolver",
    "instrument", "_internal",
}

local function assert_no_reserved_shapes(mod)
    for i = 1, #RESERVED_SHAPE_NAMES do
        local name = RESERVED_SHAPE_NAMES[i]
        local v = rawget(mod, name)
        if type(v) == "table" and rawget(v, "kind") == "shape" then
            error(string.format(
                "alc_shapes: '%s' is reserved (assert shortcut); cannot register a shape under this name",
                name), 2)
        end
    end
end

assert_no_reserved_shapes(M)

M._internal = {
    assert_no_reserved_shapes = assert_no_reserved_shapes,
    RESERVED_SHAPE_NAMES      = RESERVED_SHAPE_NAMES,
}

return M