@intentius/chant 0.19.1 → 0.21.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.
- package/dist/build-params.d.ts +108 -0
- package/dist/build-params.d.ts.map +1 -0
- package/dist/build.d.ts +79 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +31 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-examples.d.ts +41 -0
- package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -0
- package/dist/cli/commands/check-lexicon-intrinsics.d.ts +71 -0
- package/dist/cli/commands/check-lexicon-intrinsics.d.ts.map +1 -0
- package/dist/cli/commands/check-lexicon.d.ts +1 -1
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts +27 -0
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/lint.d.ts.map +1 -1
- package/dist/cli/handlers/run-client.d.ts +1 -1
- package/dist/cli/handlers/run-client.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +16 -0
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +10 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/docs-sections.d.ts.map +1 -1
- package/dist/codegen/docs-types.d.ts +2 -0
- package/dist/codegen/docs-types.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +12 -5
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/discover.d.ts +62 -7
- package/dist/components/discover.d.ts.map +1 -1
- package/dist/components/sandbox/driver.d.ts +12 -0
- package/dist/components/sandbox/driver.d.ts.map +1 -0
- package/dist/components/sandbox/run.d.ts +42 -0
- package/dist/components/sandbox/run.d.ts.map +1 -0
- package/dist/composite.d.ts +5 -0
- package/dist/composite.d.ts.map +1 -1
- package/dist/config.d.ts +71 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/declarable.d.ts +16 -0
- package/dist/declarable.d.ts.map +1 -1
- package/dist/discovery/collect.d.ts.map +1 -1
- package/dist/discovery/entity-wire-codec.d.ts +166 -0
- package/dist/discovery/entity-wire-codec.d.ts.map +1 -0
- package/dist/discovery/entity-wire.d.ts +50 -0
- package/dist/discovery/entity-wire.d.ts.map +1 -0
- package/dist/discovery/fold-import.d.ts +239 -0
- package/dist/discovery/fold-import.d.ts.map +1 -0
- package/dist/discovery/index.d.ts +74 -1
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/sandbox/bundle.d.ts +18 -0
- package/dist/discovery/sandbox/bundle.d.ts.map +1 -0
- package/dist/discovery/sandbox/child-errors.d.ts +15 -0
- package/dist/discovery/sandbox/child-errors.d.ts.map +1 -0
- package/dist/discovery/sandbox/driver.d.ts +13 -0
- package/dist/discovery/sandbox/driver.d.ts.map +1 -0
- package/dist/discovery/sandbox/run.d.ts +69 -0
- package/dist/discovery/sandbox/run.d.ts.map +1 -0
- package/dist/errors.d.ts +9 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/fold/fold.d.ts +299 -0
- package/dist/fold/fold.d.ts.map +1 -0
- package/dist/fold/foldable-helpers.d.ts +121 -0
- package/dist/fold/foldable-helpers.d.ts.map +1 -0
- package/dist/fold/subset.d.ts +134 -0
- package/dist/fold/subset.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lexicon-output.d.ts +7 -2
- package/dist/lexicon-output.d.ts.map +1 -1
- package/dist/lexicon-schema.d.ts +4 -2
- package/dist/lexicon-schema.d.ts.map +1 -1
- package/dist/lexicon.d.ts +131 -1
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.d.ts +11 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lint/component-checks.d.ts +7 -1
- package/dist/lint/component-checks.d.ts.map +1 -1
- package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
- package/dist/lint/rules/evl003-dynamic-property-access.d.ts.map +1 -1
- package/dist/params.d.ts +60 -0
- package/dist/params.d.ts.map +1 -0
- package/dist/provenance.d.ts +21 -0
- package/dist/provenance.d.ts.map +1 -1
- package/dist/runtime.d.ts +10 -1
- package/dist/runtime.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/build-params.test.ts +144 -0
- package/src/build-params.ts +207 -0
- package/src/build.test.ts +38 -0
- package/src/build.ts +144 -7
- package/src/cli/commands/build.test.ts +220 -2
- package/src/cli/commands/build.ts +111 -3
- package/src/cli/commands/check-lexicon-examples.test.ts +92 -0
- package/src/cli/commands/check-lexicon-examples.ts +103 -0
- package/src/cli/commands/check-lexicon-intrinsics.test.ts +179 -0
- package/src/cli/commands/check-lexicon-intrinsics.ts +348 -0
- package/src/cli/commands/check-lexicon.test.ts +34 -0
- package/src/cli/commands/check-lexicon.ts +137 -1
- package/src/cli/commands/lint.ts +31 -3
- package/src/cli/commands/onboard.ts +1 -1
- package/src/cli/component-security-boundary.test.ts +170 -0
- package/src/cli/handlers/build.ts +24 -3
- package/src/cli/handlers/components.ts +9 -2
- package/src/cli/handlers/dev.ts +1 -1
- package/src/cli/handlers/graph.ts +7 -5
- package/src/cli/handlers/lifecycle.ts +2 -2
- package/src/cli/handlers/lint.ts +2 -0
- package/src/cli/handlers/misc.ts +2 -2
- package/src/cli/handlers/run-client.ts +1 -1
- package/src/cli/handlers/run.ts +20 -5
- package/src/cli/main.test.ts +22 -0
- package/src/cli/main.ts +39 -0
- package/src/cli/plugins.ts +20 -4
- package/src/cli/registry.ts +10 -0
- package/src/cli/security-boundary.test.ts +135 -0
- package/src/codegen/docs-sections.test.ts +67 -0
- package/src/codegen/docs-sections.ts +7 -3
- package/src/codegen/docs-types.ts +2 -0
- package/src/components/cli-support.ts +22 -10
- package/src/components/discover.ts +127 -25
- package/src/components/sandbox/driver.ts +114 -0
- package/src/components/sandbox/run.test.ts +185 -0
- package/src/components/sandbox/run.ts +177 -0
- package/src/composite.test.ts +21 -0
- package/src/composite.ts +20 -1
- package/src/config.ts +81 -0
- package/src/declarable.ts +20 -0
- package/src/discovery/collect.ts +17 -3
- package/src/discovery/entity-wire-codec.ts +487 -0
- package/src/discovery/entity-wire.test.ts +240 -0
- package/src/discovery/entity-wire.ts +67 -0
- package/src/discovery/fold-import.test.ts +1598 -0
- package/src/discovery/fold-import.ts +1998 -0
- package/src/discovery/index.test.ts +191 -1
- package/src/discovery/index.ts +242 -1
- package/src/discovery/sandbox/bundle.ts +218 -0
- package/src/discovery/sandbox/child-errors.ts +65 -0
- package/src/discovery/sandbox/driver.ts +147 -0
- package/src/discovery/sandbox/run.test.ts +179 -0
- package/src/discovery/sandbox/run.ts +196 -0
- package/src/errors.ts +9 -1
- package/src/fold/fold.test.ts +812 -0
- package/src/fold/fold.ts +805 -0
- package/src/fold/foldable-helpers.ts +171 -0
- package/src/fold/subset-doc-parity.test.ts +210 -0
- package/src/fold/subset.test.ts +352 -0
- package/src/fold/subset.ts +383 -0
- package/src/index.ts +2 -0
- package/src/lexicon-output.ts +7 -2
- package/src/lexicon-schema.test.ts +57 -0
- package/src/lexicon-schema.ts +8 -1
- package/src/lexicon.ts +132 -1
- package/src/lifecycle/git.test.ts +10 -5
- package/src/lifecycle/release-ledger.test.ts +28 -0
- package/src/lifecycle/release-ledger.ts +11 -0
- package/src/lint/component-checks.ts +8 -1
- package/src/lint/rules/evl001-non-literal-expression.ts +26 -110
- package/src/lint/rules/evl003-dynamic-property-access.ts +11 -2
- package/src/params.test.ts +22 -0
- package/src/params.ts +66 -0
- package/src/provenance.ts +22 -0
- package/src/runtime.ts +11 -2
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* foldable-helpers — the closed, declared allowlist of chant's OWN authoring
|
|
3
|
+
* helpers that a call expression may fold through (chant #1082, epic #1019).
|
|
4
|
+
*
|
|
5
|
+
* ## Why this exists
|
|
6
|
+
*
|
|
7
|
+
* `fold()` has no general case for a `CallExpression`: a function call as a
|
|
8
|
+
* value is structurally unrepresentable there, because folding it would mean
|
|
9
|
+
* executing code, which is the one thing the fold path exists to avoid. That
|
|
10
|
+
* rule is right for user code and stays right — a user's own function, an
|
|
11
|
+
* arrow function, a method call (`naming.name(...)`) all keep failing exactly
|
|
12
|
+
* as before.
|
|
13
|
+
*
|
|
14
|
+
* But it also blocks chant's own documented authoring API. `phase("Apply",
|
|
15
|
+
* [...])` is how the component contract says to write a component; `output(ref,
|
|
16
|
+
* "oX")` is how a lexicon output is authored. An application cannot avoid them
|
|
17
|
+
* and still use components or outputs, so every file that uses one falls back
|
|
18
|
+
* to run no matter how statically evaluable the rest of it is.
|
|
19
|
+
*
|
|
20
|
+
* #1044 already settled the shape of the answer one level down, for lexicon
|
|
21
|
+
* intrinsics: a call-shaped thing may fold ONLY when it is registered, opt-in,
|
|
22
|
+
* per-helper — never because it merely looks like a call. This module is the
|
|
23
|
+
* same decision applied one level up, to chant's own helpers, with the
|
|
24
|
+
* registration written here by hand instead of coming from a lexicon manifest.
|
|
25
|
+
*
|
|
26
|
+
* ## What "registered" buys, and what still has to be true
|
|
27
|
+
*
|
|
28
|
+
* A name in this list is NOT permission to invoke whatever it happens to be
|
|
29
|
+
* bound to. Folding a helper call is a two-key operation:
|
|
30
|
+
*
|
|
31
|
+
* 1. **Shape + name** (here, and in `fold()`/`findSubsetViolation`): the
|
|
32
|
+
* callee is a bare identifier whose text is in {@link
|
|
33
|
+
* FOLDABLE_AUTHORING_HELPERS} and which is not shadowed by a local
|
|
34
|
+
* `const`. `fold()` reduces the call to a symbolic
|
|
35
|
+
* `{ __helper, args }` envelope — it still executes nothing.
|
|
36
|
+
* 2. **Provenance + invocation** (`../discovery/fold-import.ts`): the
|
|
37
|
+
* envelope is revived by resolving that name through the folding FILE'S
|
|
38
|
+
* OWN `import` bindings and checking the import actually comes from chant
|
|
39
|
+
* ({@link isChantOwnedSpecifier}, or a path inside chant-core's own tree
|
|
40
|
+
* for in-repo/absolute-specifier callers). Only then is the real function
|
|
41
|
+
* invoked, with the real folded arguments. A same-named helper imported
|
|
42
|
+
* from somewhere else, or declared in the file itself, resolves to
|
|
43
|
+
* nothing chant owns and the whole file falls back to run.
|
|
44
|
+
*
|
|
45
|
+
* So the function that runs is always the same function the run path would
|
|
46
|
+
* have called, from the same module the source itself imported — fold does not
|
|
47
|
+
* substitute its own reimplementation, which is why this list cannot drift
|
|
48
|
+
* from the helpers' real behavior.
|
|
49
|
+
*
|
|
50
|
+
* ## Admission criteria
|
|
51
|
+
*
|
|
52
|
+
* A helper belongs here only if all of these hold:
|
|
53
|
+
*
|
|
54
|
+
* - chant owns and documents it as authoring surface;
|
|
55
|
+
* - it is a pure function of its arguments — no I/O, no `process.env`, no
|
|
56
|
+
* module-level mutable state, no observable side effect;
|
|
57
|
+
* - calling it early (at fold time) is indistinguishable from calling it
|
|
58
|
+
* during a real run of the file.
|
|
59
|
+
*
|
|
60
|
+
* Deliberately NOT admitted — see this module's tests and the #1082 PR body:
|
|
61
|
+
*
|
|
62
|
+
* - `env()` (`../env.ts`) reads `process.env`. Folding it would bake one
|
|
63
|
+
* run's environment into a statically-derived value. It is exactly the
|
|
64
|
+
* kind of call fold must keep rejecting.
|
|
65
|
+
* - `Op()` (`../op/builders.ts`) constructs an `OpResource` — a `Declarable`.
|
|
66
|
+
* `fold()` already rejects a nested `new Type(...)` used as a value for a
|
|
67
|
+
* real, differential-caught reason (the envelope leaks into
|
|
68
|
+
* serialization); a factory that returns one is the same hazard wearing a
|
|
69
|
+
* call.
|
|
70
|
+
* - `propagate()`, `withDefaults()`, `resource()`, `mergeDefaults()`
|
|
71
|
+
* (`../composite.ts`) are composite *definition* helpers, not value-position
|
|
72
|
+
* helpers. `propagate()` in particular mutates its argument in place, and
|
|
73
|
+
* the composite spine in fold-import.ts already resolves it live.
|
|
74
|
+
* - `createResource()`/`createProperty()` (`../runtime.ts`) are used at a
|
|
75
|
+
* lexicon module's top level to build classes, never as a value inside a
|
|
76
|
+
* project file.
|
|
77
|
+
* - Lexicon intrinsics in CALL form (`Ref(...)`, `Join(...)`) are lexicon
|
|
78
|
+
* surface, not chant's own, and are #1044's registry (`IntrinsicDef`,
|
|
79
|
+
* `../lexicon.ts`) to admit — not this one. Only their tagged-template
|
|
80
|
+
* form folds today.
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* One registered helper. `module` and `note` carry no runtime behavior — they
|
|
85
|
+
* are the audit trail for why this entry passed the admission criteria above,
|
|
86
|
+
* kept next to the name it justifies rather than in a comment that can drift
|
|
87
|
+
* away from the list.
|
|
88
|
+
*/
|
|
89
|
+
export interface FoldableHelperDef {
|
|
90
|
+
/** The exported name, matched against the callee identifier's text. */
|
|
91
|
+
readonly name: string;
|
|
92
|
+
/** Where chant defines it (a path under `packages/core/src`, for the audit trail). */
|
|
93
|
+
readonly module: string;
|
|
94
|
+
/** Why it qualifies — what it returns and why calling it at fold time is safe. */
|
|
95
|
+
readonly note: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The allowlist. Adding an entry is a deliberate act: it must satisfy every
|
|
100
|
+
* admission criterion in this module's doc, and it widens what `fold()` and
|
|
101
|
+
* `findSubsetViolation` accept for EVERY project, so it belongs in a PR that
|
|
102
|
+
* says so.
|
|
103
|
+
*
|
|
104
|
+
* Two names below are defined TWICE in chant, by different modules, with
|
|
105
|
+
* different return types (`phase`/`gate` by both the component contract and
|
|
106
|
+
* the Op builders; `stackOutput` by both the component contract and the
|
|
107
|
+
* cross-stack output primitive). That is fine and needs no disambiguation
|
|
108
|
+
* here: registration is by name, but the function actually invoked is the one
|
|
109
|
+
* the folding file itself imported (see step 2 in the module doc), so each
|
|
110
|
+
* file gets its own. Both definitions of each name independently satisfy the
|
|
111
|
+
* criteria, which is what makes registering the shared name safe.
|
|
112
|
+
*/
|
|
113
|
+
export const FOLDABLE_AUTHORING_HELPERS: readonly FoldableHelperDef[] = [
|
|
114
|
+
{
|
|
115
|
+
name: "phase",
|
|
116
|
+
module: "components/component.ts, op/builders.ts",
|
|
117
|
+
note: "Returns a plain `{ phase, steps, parallel? }` / `{ name, steps, parallel? }` object literal built from its arguments. No state, no I/O.",
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: "gate",
|
|
121
|
+
module: "components/component.ts, op/builders.ts",
|
|
122
|
+
note: "Returns a plain `{ kind: 'gate', signalName, ... }` object literal built from its arguments.",
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: "activity",
|
|
126
|
+
module: "op/builders.ts",
|
|
127
|
+
note: "Returns a plain `{ kind: 'activity', fn, args?, profile? }` object literal built from its arguments.",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: "stackOutput",
|
|
131
|
+
module: "components/component.ts, stack-output.ts",
|
|
132
|
+
note: "Component form returns a plain `{ stackOutput: { stack, name } }` literal. Cross-stack form derives a `StackOutput` from a real `AttrRef`/`Intrinsic` and throws on anything else — so a fold that only has a symbolic reference fails loudly into run-fallback rather than producing a wrong output.",
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: "output",
|
|
136
|
+
module: "lexicon-output.ts",
|
|
137
|
+
note: "Constructs a `LexiconOutput` from a real `AttrRef`/`Intrinsic` and a name. Pure, but identity-sensitive: it reads through the ref's `WeakRef` to its parent entity. Only folds when the ref argument revives to a REAL live reference (see fold-import.ts's `requireLiveRefs`); a symbolic `{ __attrRef }` envelope is rejected, not silently wrapped.",
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
const HELPER_NAMES: ReadonlySet<string> = new Set(FOLDABLE_AUTHORING_HELPERS.map((h) => h.name));
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* True when `name` is a registered foldable authoring helper. Name-only — this
|
|
145
|
+
* is the shape-level half of the check (step 1 in the module doc). It says
|
|
146
|
+
* nothing about where the name is bound; `../discovery/fold-import.ts` decides
|
|
147
|
+
* that before anything is invoked.
|
|
148
|
+
*/
|
|
149
|
+
export function isFoldableHelperName(name: string): boolean {
|
|
150
|
+
return HELPER_NAMES.has(name);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Package specifiers chant itself publishes. A registered helper name only
|
|
155
|
+
* folds when the folding file imported it from one of these — or, for in-repo
|
|
156
|
+
* and test callers that import chant-core by relative/absolute path, from
|
|
157
|
+
* inside chant-core's own tree (checked separately, in fold-import.ts, since
|
|
158
|
+
* only that module knows how to resolve a specifier to a path).
|
|
159
|
+
*
|
|
160
|
+
* Lexicon packages are included because several core helpers are re-exported
|
|
161
|
+
* through them and that is the documented import in real projects — e.g.
|
|
162
|
+
* `import { output } from "@intentius/chant-lexicon-aws"`.
|
|
163
|
+
*/
|
|
164
|
+
const CHANT_PACKAGE_SPECIFIERS: readonly string[] = ["@intentius/chant", "@intentius/chant-lexicon-"];
|
|
165
|
+
|
|
166
|
+
/** True for a bare specifier that names a chant-published package (or one of its subpaths). */
|
|
167
|
+
export function isChantOwnedSpecifier(specifier: string): boolean {
|
|
168
|
+
return CHANT_PACKAGE_SPECIFIERS.some(
|
|
169
|
+
(prefix) => specifier === prefix || specifier.startsWith(prefix.endsWith("-") ? prefix : `${prefix}/`),
|
|
170
|
+
);
|
|
171
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import * as ts from "typescript";
|
|
3
|
+
import { readFileSync } from "fs";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { collectConsts } from "./fold";
|
|
7
|
+
import { findSubsetViolation } from "./subset";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* subset-doc-parity.test.ts — chant #1062 (epic #1019).
|
|
11
|
+
*
|
|
12
|
+
* `docs/.../concepts/typescript-as-data.mdx`'s "Supported Patterns" and
|
|
13
|
+
* "Unsupported Patterns" sections are prose over `findSubsetViolation`
|
|
14
|
+
* (./subset.ts), the real, code-defined classifier. #1062 exists because
|
|
15
|
+
* that prose already drifted from the code once (see subset.ts's own
|
|
16
|
+
* module doc, and #1061's fix) and nothing stopped it.
|
|
17
|
+
*
|
|
18
|
+
* Full generation was considered and rejected: `findSubsetViolation` is a
|
|
19
|
+
* recursive-descent classifier over `ts.Node` kinds, not a flat table — its
|
|
20
|
+
* value is precisely the nuance a table would lose (flow-insensitive
|
|
21
|
+
* short-circuit handling, opaque tagged-template interiors, which EVL rule
|
|
22
|
+
* id a rejection maps to, …). Mechanically reverse-engineering that nuance
|
|
23
|
+
* from the function body would mean either emitting a bare list of
|
|
24
|
+
* `ts.SyntaxKind` names (useless to a reader) or writing a second, brittle
|
|
25
|
+
* introspector that breaks on any harmless refactor of subset.ts — a worse
|
|
26
|
+
* failure mode than the one this issue exists to fix.
|
|
27
|
+
*
|
|
28
|
+
* Instead: this test pulls each pattern's example STRAIGHT OUT of the doc's
|
|
29
|
+
* own fenced code block (by heading, via {@link extractFencedBlock}) — no
|
|
30
|
+
* second copy of the snippet lives in this file — and runs it through
|
|
31
|
+
* `findSubsetViolation` for real. A heading that's renamed or removed fails
|
|
32
|
+
* loudly (`heading not found`); a snippet whose real verdict no longer
|
|
33
|
+
* matches what the doc claims for it fails just as loudly. That is the
|
|
34
|
+
* concrete failure mode from the day this issue was filed, closed for good.
|
|
35
|
+
*
|
|
36
|
+
* Scope: only patterns `findSubsetViolation` itself decides (EVL001/EVL003 —
|
|
37
|
+
* see subset.ts's module doc for why those are the two it owns). Left out,
|
|
38
|
+
* deliberately:
|
|
39
|
+
* - "Control flow around resources" (EVL002) and the `let`/`var`, `class`,
|
|
40
|
+
* `require()`, top-level `await`, decorator bullets under "Other
|
|
41
|
+
* unsupported patterns" — these are rejected at `foldModule`'s top-level
|
|
42
|
+
* statement scan, before any expression ever reaches
|
|
43
|
+
* `findSubsetViolation`. Not this module's concern, and not real,
|
|
44
|
+
* parseable TypeScript in the doc's own illustrative snippet (a bare
|
|
45
|
+
* `export` inside an `if` block isn't valid syntax to begin with).
|
|
46
|
+
* - "Import and re-export" / "Cross-file resource references" / "Typed
|
|
47
|
+
* property-kind constructors" are exercised below, but only prove the
|
|
48
|
+
* trivial case: `findSubsetViolation` treats every identifier as
|
|
49
|
+
* shape-valid regardless of whether it resolves locally, across a
|
|
50
|
+
* module boundary, or not at all (subset.ts's module doc, point 1) — a
|
|
51
|
+
* documented, intentional asymmetry, not something this guard can
|
|
52
|
+
* usefully narrow further without a binding resolver of its own.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
const repoRoot = fileURLToPath(new URL("../../../../", import.meta.url));
|
|
56
|
+
const docPath = join(repoRoot, "docs", "src", "content", "docs", "concepts", "typescript-as-data.mdx");
|
|
57
|
+
const doc = readFileSync(docPath, "utf-8");
|
|
58
|
+
|
|
59
|
+
/** Pull the first ```typescript fenced block following `### {heading}`. */
|
|
60
|
+
function extractFencedBlock(heading: string): string {
|
|
61
|
+
const headingMarker = `### ${heading}`;
|
|
62
|
+
const headingIdx = doc.indexOf(headingMarker);
|
|
63
|
+
if (headingIdx === -1) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`subset-doc-parity: heading "${headingMarker}" not found in ${docPath} — was it renamed or removed? Update this test to match.`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
const fenceStart = doc.indexOf("```typescript", headingIdx);
|
|
69
|
+
if (fenceStart === -1) {
|
|
70
|
+
throw new Error(`subset-doc-parity: no \`\`\`typescript block found after "${headingMarker}"`);
|
|
71
|
+
}
|
|
72
|
+
const codeStart = doc.indexOf("\n", fenceStart) + 1;
|
|
73
|
+
const fenceEnd = doc.indexOf("```", codeStart);
|
|
74
|
+
if (fenceEnd === -1) {
|
|
75
|
+
throw new Error(`subset-doc-parity: unterminated code fence after "${headingMarker}"`);
|
|
76
|
+
}
|
|
77
|
+
return doc.slice(codeStart, fenceEnd);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Parse `source` and return its top-level `const` bindings, keyed by name. */
|
|
81
|
+
function parseConsts(source: string): Map<string, ts.Expression> {
|
|
82
|
+
const sourceFile = ts.createSourceFile("doc-example.ts", source, ts.ScriptTarget.Latest, /* setParentNodes */ true);
|
|
83
|
+
return collectConsts(sourceFile);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The object-literal argument of `const <name> = new Type({...})`. */
|
|
87
|
+
function resourceArg(consts: Map<string, ts.Expression>, name: string): ts.Expression {
|
|
88
|
+
const init = consts.get(name);
|
|
89
|
+
if (!init || !ts.isNewExpression(init) || !init.arguments?.[0]) {
|
|
90
|
+
throw new Error(`subset-doc-parity: "${name}" did not parse as a resource declaration with an object-literal arg`);
|
|
91
|
+
}
|
|
92
|
+
return init.arguments[0];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
describe("subset-doc-parity — supported patterns in typescript-as-data.mdx classify as fold-clean", () => {
|
|
96
|
+
test("Resource declarations", () => {
|
|
97
|
+
const consts = parseConsts(extractFencedBlock("Resource declarations"));
|
|
98
|
+
expect(findSubsetViolation(resourceArg(consts, "store"))).toBeUndefined();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("Literal values", () => {
|
|
102
|
+
const consts = parseConsts(extractFencedBlock("Literal values"));
|
|
103
|
+
expect(findSubsetViolation(resourceArg(consts, "service"))).toBeUndefined();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("Const variable references", () => {
|
|
107
|
+
const consts = parseConsts(extractFencedBlock("Const variable references"));
|
|
108
|
+
expect(findSubsetViolation(resourceArg(consts, "store"))).toBeUndefined();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("Spread from const sources", () => {
|
|
112
|
+
const consts = parseConsts(extractFencedBlock("Spread from const sources"));
|
|
113
|
+
expect(findSubsetViolation(resourceArg(consts, "service"))).toBeUndefined();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("Import and re-export", () => {
|
|
117
|
+
// Trivial by construction — see this file's module doc.
|
|
118
|
+
const consts = parseConsts(extractFencedBlock("Import and re-export"));
|
|
119
|
+
expect(findSubsetViolation(resourceArg(consts, "store"))).toBeUndefined();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("Cross-file resource references", () => {
|
|
123
|
+
// Trivial by construction — see this file's module doc.
|
|
124
|
+
const consts = parseConsts(extractFencedBlock("Cross-file resource references"));
|
|
125
|
+
expect(findSubsetViolation(resourceArg(consts, "service"))).toBeUndefined();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("Intrinsic tagged templates", () => {
|
|
129
|
+
// subset.ts treats any tagged template's TAG as shape-valid regardless
|
|
130
|
+
// of lexicon registration (that check is fold()'s job, not subset.ts's —
|
|
131
|
+
// see subset.ts's module doc, point 2) — only the interpolated values
|
|
132
|
+
// are classified, and this example's interpolation is a plain property
|
|
133
|
+
// access chain.
|
|
134
|
+
const consts = parseConsts(extractFencedBlock("Intrinsic tagged templates"));
|
|
135
|
+
expect(findSubsetViolation(resourceArg(consts, "store"))).toBeUndefined();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("Registered intrinsic calls", () => {
|
|
139
|
+
// chant #1044 — the one doc snippet that needs the registry to classify:
|
|
140
|
+
// `Ref(...)` is only in the subset because a lexicon opted its call form
|
|
141
|
+
// in, and `findSubsetViolation` answers exactly that question when it is
|
|
142
|
+
// given the registry (subset.ts module doc, point 2c). Without one it
|
|
143
|
+
// would report a violation, which is the pre-#1044 answer EVL still gets.
|
|
144
|
+
const consts = parseConsts(extractFencedBlock("Registered intrinsic calls"));
|
|
145
|
+
const intrinsics = [
|
|
146
|
+
{ name: "Sub", isTag: true },
|
|
147
|
+
{ name: "Ref", isTag: false, foldsAsCall: true },
|
|
148
|
+
];
|
|
149
|
+
expect(findSubsetViolation(resourceArg(consts, "store"), intrinsics)).toBeUndefined();
|
|
150
|
+
expect(findSubsetViolation(resourceArg(consts, "store"))).toBeDefined();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("Typed property-kind constructors", () => {
|
|
154
|
+
const consts = parseConsts(extractFencedBlock("Typed property-kind constructors"));
|
|
155
|
+
expect(findSubsetViolation(resourceArg(consts, "config"))).toBeUndefined();
|
|
156
|
+
expect(findSubsetViolation(resourceArg(consts, "access"))).toBeUndefined();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("Registered authoring helpers", () => {
|
|
160
|
+
// chant #1082 — not a `new Type({...})` declaration, so classify the
|
|
161
|
+
// exported component object literal directly. `findSubsetViolation`
|
|
162
|
+
// checks the helper NAME only (subset.ts module doc, point 2b); the
|
|
163
|
+
// doc's own snippet imports them from chant, which is what the bridge
|
|
164
|
+
// additionally verifies at fold time.
|
|
165
|
+
const consts = parseConsts(extractFencedBlock("Registered authoring helpers"));
|
|
166
|
+
const web = consts.get("web");
|
|
167
|
+
if (!web) throw new Error(`subset-doc-parity: "web" did not parse as a const declaration`);
|
|
168
|
+
expect(findSubsetViolation(web)).toBeUndefined();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("Nullish coalescing for defaults", () => {
|
|
172
|
+
// Not a standalone statement in the doc (a single object-literal
|
|
173
|
+
// property, deliberately shown as a fragment) — wrapped in an object
|
|
174
|
+
// literal to parse, which also happens to be exactly the shape
|
|
175
|
+
// `findSubsetViolation` classifies a resource prop through.
|
|
176
|
+
const fragment = extractFencedBlock("Nullish coalescing for defaults").trim();
|
|
177
|
+
const consts = parseConsts(`const _wrapped = { ${fragment} };`);
|
|
178
|
+
const wrapped = consts.get("_wrapped");
|
|
179
|
+
if (!wrapped) throw new Error("subset-doc-parity: nullish-coalescing fragment failed to parse");
|
|
180
|
+
expect(findSubsetViolation(wrapped)).toBeUndefined();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("subset-doc-parity — unsupported patterns in typescript-as-data.mdx classify as rejected", () => {
|
|
185
|
+
test("Function calls as values", () => {
|
|
186
|
+
const consts = parseConsts(extractFencedBlock("Function calls as values"));
|
|
187
|
+
expect(findSubsetViolation(resourceArg(consts, "store"))).toBeDefined();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("Dynamic property access", () => {
|
|
191
|
+
// A bare `const`, not a resource declaration — check the initializer
|
|
192
|
+
// directly rather than through `resourceArg`.
|
|
193
|
+
const consts = parseConsts(extractFencedBlock("Dynamic property access"));
|
|
194
|
+
const init = consts.get("name");
|
|
195
|
+
if (!init) throw new Error(`subset-doc-parity: "name" did not parse as a const declaration`);
|
|
196
|
+
expect(findSubsetViolation(init)).toBeDefined();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("Spread from dynamic sources", () => {
|
|
200
|
+
// Documented as EVL004 (a stricter, value-level narrowing rule subset.ts
|
|
201
|
+
// doesn't model — see subset.ts's module doc, item 3) but this
|
|
202
|
+
// particular example — spreading a CALL's result — is also rejected by
|
|
203
|
+
// subset.ts's shape classifier on its own terms: a call expression
|
|
204
|
+
// nested inside a spread is still a call expression. Asserting only
|
|
205
|
+
// "rejected", not a specific rule id, since attributing a rule id here
|
|
206
|
+
// is EVL004's job, not subset.ts's.
|
|
207
|
+
const consts = parseConsts(extractFencedBlock("Spread from dynamic sources"));
|
|
208
|
+
expect(findSubsetViolation(resourceArg(consts, "store"))).toBeDefined();
|
|
209
|
+
});
|
|
210
|
+
});
|