@intentius/tsad-conformance 1.4.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/README.md +59 -0
- package/dist/adapter.d.ts +65 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +2 -0
- package/dist/adapter.js.map +1 -0
- package/dist/bundled.d.ts +5 -0
- package/dist/bundled.d.ts.map +1 -0
- package/dist/bundled.js +28 -0
- package/dist/bundled.js.map +1 -0
- package/dist/fixture.d.ts +49 -0
- package/dist/fixture.d.ts.map +1 -0
- package/dist/fixture.js +95 -0
- package/dist/fixture.js.map +1 -0
- package/dist/host.d.ts +33 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +79 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/rules.d.ts +46 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +135 -0
- package/dist/rules.js.map +1 -0
- package/dist/runner.d.ts +26 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +152 -0
- package/dist/runner.js.map +1 -0
- package/fixtures/F-CallLeak/a-call-returns-a-live-object/expect.json +8 -0
- package/fixtures/F-CallLeak/a-call-returns-a-live-object/project/caller.ts +5 -0
- package/fixtures/F-CallLeak/a-call-returns-a-live-object/project/leaky.ts +7 -0
- package/fixtures/F-CallLeak/a-call-returns-a-live-object/project/pure.ts +5 -0
- package/fixtures/F-Cycle/mutual-imports/expect.json +6 -0
- package/fixtures/F-Cycle/mutual-imports/project/a.ts +3 -0
- package/fixtures/F-Cycle/mutual-imports/project/b.ts +3 -0
- package/fixtures/F-Depth/exhaustion-is-a-fallback/expect.json +19 -0
- package/fixtures/F-Depth/exhaustion-is-a-fallback/project/deep.ts +3 -0
- package/fixtures/F-Depth/exhaustion-is-a-fallback/project/rec.ts +3 -0
- package/fixtures/F-Div-Depth/new-inside-function-body/expect.json +21 -0
- package/fixtures/F-Div-Depth/new-inside-function-body/project/app.ts +3 -0
- package/fixtures/F-Div-Depth/new-inside-function-body/project/fn.ts +3 -0
- package/fixtures/F-Div-Eager/referenced-not-called/expect.json +8 -0
- package/fixtures/F-Div-Eager/referenced-not-called/project/app.ts +3 -0
- package/fixtures/F-Div-NsNew/namespace-callee/expect.json +18 -0
- package/fixtures/F-Div-NsNew/namespace-callee/input.ts +3 -0
- package/fixtures/F-Div-Provenance/helper-name-from-project-import/expect.json +20 -0
- package/fixtures/F-Div-Provenance/helper-name-from-project-import/project/app.ts +3 -0
- package/fixtures/F-Div-Provenance/helper-name-from-project-import/project/local.ts +3 -0
- package/fixtures/F-Div-SameFileNew/no-module-graph/expect.json +8 -0
- package/fixtures/F-Div-SameFileNew/no-module-graph/input.ts +5 -0
- package/fixtures/F-Div-Tag/unregistered-tag/expect.json +15 -0
- package/fixtures/F-Div-Tag/unregistered-tag/input.ts +1 -0
- package/fixtures/F-Eval-Array/spread-string-rejected/expect.json +14 -0
- package/fixtures/F-Eval-Array/spread-string-rejected/input.ts +1 -0
- package/fixtures/F-Eval-Binary/division-and-float/expect.json +19 -0
- package/fixtures/F-Eval-Binary/division-and-float/input.ts +1 -0
- package/fixtures/F-Eval-Binary/logical-return-operand/expect.json +20 -0
- package/fixtures/F-Eval-Binary/logical-return-operand/input.ts +1 -0
- package/fixtures/F-Eval-Binary/plus-dispatch/expect.json +19 -0
- package/fixtures/F-Eval-Binary/plus-dispatch/input.ts +1 -0
- package/fixtures/F-Eval-Binary/relational-on-strings/expect.json +18 -0
- package/fixtures/F-Eval-Binary/relational-on-strings/input.ts +1 -0
- package/fixtures/F-Eval-CallEager/evaluated-at-fold-time/expect.json +8 -0
- package/fixtures/F-Eval-CallEager/evaluated-at-fold-time/project/app.ts +3 -0
- package/fixtures/F-Eval-CallEager/not-a-function/expect.json +22 -0
- package/fixtures/F-Eval-CallEager/not-a-function/project/app.ts +3 -0
- package/fixtures/F-Eval-CallEager/not-a-function/project/local.ts +1 -0
- package/fixtures/F-Eval-CallHelper/helper-folds/expect.json +8 -0
- package/fixtures/F-Eval-CallHelper/helper-folds/project/app.ts +3 -0
- package/fixtures/F-Eval-CallHelper/inside-function-body/expect.json +22 -0
- package/fixtures/F-Eval-CallHelper/inside-function-body/project/app.ts +3 -0
- package/fixtures/F-Eval-CallHelper/inside-function-body/project/fn.ts +5 -0
- package/fixtures/F-Eval-CallIntrinsic/call-form-folds/expect.json +8 -0
- package/fixtures/F-Eval-CallIntrinsic/call-form-folds/project/app.ts +3 -0
- package/fixtures/F-Eval-CallIntrinsic/inside-function-body/expect.json +22 -0
- package/fixtures/F-Eval-CallIntrinsic/inside-function-body/project/app.ts +3 -0
- package/fixtures/F-Eval-CallIntrinsic/inside-function-body/project/fn.ts +5 -0
- package/fixtures/F-Eval-CallLocal/local-function-folds/expect.json +7 -0
- package/fixtures/F-Eval-CallLocal/local-function-folds/project/app.ts +3 -0
- package/fixtures/F-Eval-CallLocal/local-function-folds/project/pure.ts +3 -0
- package/fixtures/F-Eval-CallLocal/recursion-folds/expect.json +17 -0
- package/fixtures/F-Eval-CallLocal/recursion-folds/project/rec.ts +3 -0
- package/fixtures/F-Eval-CallLocal/recursion-folds/project/shallow.ts +3 -0
- package/fixtures/F-Eval-CallLocal/spread-argument-rejected/expect.json +20 -0
- package/fixtures/F-Eval-CallLocal/spread-argument-rejected/project/app.ts +5 -0
- package/fixtures/F-Eval-CallLocal/spread-argument-rejected/project/pure.ts +3 -0
- package/fixtures/F-Eval-CallMethod/method-missing/expect.json +17 -0
- package/fixtures/F-Eval-CallMethod/method-missing/input.ts +1 -0
- package/fixtures/F-Eval-CallMethod/null-receiver-dot/expect.json +13 -0
- package/fixtures/F-Eval-CallMethod/null-receiver-dot/input.ts +2 -0
- package/fixtures/F-Eval-CallMethod/null-receiver-optional/expect.json +2 -0
- package/fixtures/F-Eval-CallMethod/null-receiver-optional/input.ts +2 -0
- package/fixtures/F-Eval-CallMethod/real-receiver/expect.json +2 -0
- package/fixtures/F-Eval-CallMethod/real-receiver/input.ts +1 -0
- package/fixtures/F-Eval-Template/number-formatting/expect.json +11 -0
- package/fixtures/F-Eval-Template/number-formatting/input.ts +1 -0
- package/fixtures/F-Eval-Template/tostring-of-non-numbers/expect.json +10 -0
- package/fixtures/F-Eval-Template/tostring-of-non-numbers/input.ts +1 -0
- package/fixtures/F-Eval-Unary/coercion/expect.json +19 -0
- package/fixtures/F-Eval-Unary/coercion/input.ts +1 -0
- package/fixtures/F-Eval-Undefined/bare-undefined/expect.json +2 -0
- package/fixtures/F-Eval-Undefined/bare-undefined/input.ts +1 -0
- package/fixtures/F-Exc-Registry/call-form-without-registry/expect.json +15 -0
- package/fixtures/F-Exc-Registry/call-form-without-registry/input.ts +1 -0
- package/fixtures/F-Host-Closed-vs-Open/package-call-is-closed/expect.json +9 -0
- package/fixtures/F-Host-Closed-vs-Open/package-call-is-closed/project/closed.ts +3 -0
- package/fixtures/F-Host-Closed-vs-Open/package-call-is-closed/project/open.ts +3 -0
- package/fixtures/F-Host-Closed-vs-Open/package-call-is-closed/project/pure.ts +3 -0
- package/fixtures/F-Host-DataExports/plain-data-export/expect.json +8 -0
- package/fixtures/F-Host-DataExports/plain-data-export/project/app.ts +3 -0
- package/fixtures/F-Host-Registry/tag-called-as-function/expect.json +8 -0
- package/fixtures/F-Host-Registry/tag-called-as-function/project/app.ts +3 -0
- package/fixtures/F-Host-Trust/unowned-package/expect.json +6 -0
- package/fixtures/F-Host-Trust/unowned-package/project/app.ts +3 -0
- package/fixtures/F-Namespace/synthetic-namespace-object/expect.json +7 -0
- package/fixtures/F-Namespace/synthetic-namespace-object/project/use.ts +3 -0
- package/fixtures/F-Namespace/synthetic-namespace-object/project/values.ts +2 -0
- package/fixtures/F-NoExports/nothing-to-produce/expect.json +7 -0
- package/fixtures/F-NoExports/nothing-to-produce/project/defaulted.ts +1 -0
- package/fixtures/F-NoExports/nothing-to-produce/project/empty.ts +3 -0
- package/fixtures/F-Prebuild/same-file-construction-shared/expect.json +19 -0
- package/fixtures/F-Prebuild/same-file-construction-shared/project/shared.ts +6 -0
- package/fixtures/F-Profile-DataHost/plain-data-across-files/expect.json +39 -0
- package/fixtures/F-Profile-DataHost/plain-data-across-files/project/defaults.ts +5 -0
- package/fixtures/F-Profile-DataHost/plain-data-across-files/project/service.ts +10 -0
- package/fixtures/F-Seed/seeded-by-its-own-body/expect.json +8 -0
- package/fixtures/F-Seed/seeded-by-its-own-body/project/app.ts +8 -0
- package/fixtures/F-Seed/seeded-by-its-own-body/project/lone.ts +1 -0
- package/fixtures/F-Succ/backward-along-captures/expect.json +26 -0
- package/fixtures/F-Succ/backward-along-captures/project/breaker.ts +10 -0
- package/fixtures/F-Succ/backward-along-captures/project/shared.ts +1 -0
- package/fixtures/F-Succ/backward-along-captures/project/sibling.ts +3 -0
- package/fixtures/F-Succ/forward-into-imports/expect.json +8 -0
- package/fixtures/F-Succ/forward-into-imports/project/app.ts +10 -0
- package/fixtures/F-Succ/forward-into-imports/project/config.ts +1 -0
- package/fixtures/F-Taint/independent-file-still-folds/expect.json +9 -0
- package/fixtures/F-Taint/independent-file-still-folds/project/alone.ts +1 -0
- package/fixtures/F-Taint/independent-file-still-folds/project/app.ts +8 -0
- package/fixtures/F-Total/one-bad-declarator-fails-the-file/expect.json +7 -0
- package/fixtures/F-Total/one-bad-declarator-fails-the-file/project/mixed.ts +3 -0
- package/fixtures/F-Val-Arity/props-attributes-and-spread/expect.json +14 -0
- package/fixtures/F-Val-Arity/props-attributes-and-spread/project/arities.ts +5 -0
- package/fixtures/F-Val-Callable/host-function-as-value/expect.json +16 -0
- package/fixtures/F-Val-Callable/host-function-as-value/project/app.ts +3 -0
- package/fixtures/F-Val-Fate/revive-every-envelope/expect.json +15 -0
- package/fixtures/F-Val-Fate/revive-every-envelope/project/entities.ts +6 -0
- package/fixtures/F-Val-Live/plain-data-is-not-live/expect.json +9 -0
- package/fixtures/F-Val-Live/plain-data-is-not-live/project/breaker.ts +10 -0
- package/fixtures/F-Val-Live/plain-data-is-not-live/project/quiet.ts +3 -0
- package/fixtures/F-Val-Live/plain-data-is-not-live/project/source.ts +5 -0
- package/fixtures/F-Val-Position/attrref-in-host-arguments/expect.json +8 -0
- package/fixtures/F-Val-Position/attrref-in-host-arguments/project/rejected.ts +4 -0
- package/fixtures/F-Val-Undefined/array-element-undefined/expect.json +15 -0
- package/fixtures/F-Val-Undefined/array-element-undefined/input.ts +1 -0
- package/fixtures/F-Val-Undefined/conditional-yields-undefined/expect.json +13 -0
- package/fixtures/F-Val-Undefined/conditional-yields-undefined/input.ts +2 -0
- package/fixtures/F-Val-Undefined/optional-chain-yields-undefined-property/expect.json +13 -0
- package/fixtures/F-Val-Undefined/optional-chain-yields-undefined-property/input.ts +2 -0
- package/fixtures/F-Val-Undefined/property-literal-undefined/expect.json +16 -0
- package/fixtures/F-Val-Undefined/property-literal-undefined/input.ts +1 -0
- package/fixtures/F-Val-Undefined/spread-carries-undefined/expect.json +14 -0
- package/fixtures/F-Val-Undefined/spread-carries-undefined/input.ts +2 -0
- package/fixtures/S-Array/spread/expect.json +1 -0
- package/fixtures/S-Array/spread/input.ts +1 -0
- package/fixtures/S-Binary/arithmetic-and-concat/expect.json +1 -0
- package/fixtures/S-Binary/arithmetic-and-concat/input.ts +1 -0
- package/fixtures/S-Binary/lazy-untaken-unfoldable/expect.json +1 -0
- package/fixtures/S-Binary/lazy-untaken-unfoldable/input.ts +1 -0
- package/fixtures/S-Binary/nullish-coalesce/expect.json +1 -0
- package/fixtures/S-Binary/nullish-coalesce/input.ts +1 -0
- package/fixtures/S-CallLocal/imported-callee-is-shape-valid/expect.json +10 -0
- package/fixtures/S-CallLocal/imported-callee-is-shape-valid/input.ts +3 -0
- package/fixtures/S-Conditional/taken-branch-only/expect.json +1 -0
- package/fixtures/S-Conditional/taken-branch-only/input.ts +1 -0
- package/fixtures/S-ExportDefault/imported-by-sibling/expect.json +30 -0
- package/fixtures/S-ExportDefault/imported-by-sibling/project/app.ts +3 -0
- package/fixtures/S-ExportDefault/imported-by-sibling/project/policy.ts +1 -0
- package/fixtures/S-ExportDefault/plain-object/expect.json +21 -0
- package/fixtures/S-ExportDefault/plain-object/input.ts +1 -0
- package/fixtures/S-Ident/const-chain/expect.json +1 -0
- package/fixtures/S-Ident/const-chain/input.ts +1 -0
- package/fixtures/S-Ident/process/expect.json +1 -0
- package/fixtures/S-Ident/process/input.ts +1 -0
- package/fixtures/S-Ident/unresolved/expect.json +1 -0
- package/fixtures/S-Ident/unresolved/input.ts +1 -0
- package/fixtures/S-Index/dynamic-key/expect.json +1 -0
- package/fixtures/S-Index/dynamic-key/input.ts +1 -0
- package/fixtures/S-Index/literal-key/expect.json +1 -0
- package/fixtures/S-Index/literal-key/input.ts +1 -0
- package/fixtures/S-Literal/null-and-booleans/expect.json +1 -0
- package/fixtures/S-Literal/null-and-booleans/input.ts +1 -0
- package/fixtures/S-Literal/numeric/expect.json +1 -0
- package/fixtures/S-Literal/numeric/input.ts +1 -0
- package/fixtures/S-LocalFunction/arrow-const-call/expect.json +21 -0
- package/fixtures/S-LocalFunction/arrow-const-call/project/label.ts +3 -0
- package/fixtures/S-LocalFunction/function-as-value-still-rejects/expect.json +11 -0
- package/fixtures/S-LocalFunction/function-as-value-still-rejects/input.ts +3 -0
- package/fixtures/S-LocalFunction/same-file-declaration-call/expect.json +19 -0
- package/fixtures/S-LocalFunction/same-file-declaration-call/project/twice.ts +5 -0
- package/fixtures/S-Member/nested/expect.json +1 -0
- package/fixtures/S-Member/nested/input.ts +1 -0
- package/fixtures/S-Member/nullish-plain-refuses/expect.json +1 -0
- package/fixtures/S-Member/nullish-plain-refuses/input.ts +1 -0
- package/fixtures/S-Member/optional-chain/expect.json +1 -0
- package/fixtures/S-Member/optional-chain/input.ts +1 -0
- package/fixtures/S-Object/spread-later-wins/expect.json +1 -0
- package/fixtures/S-Object/spread-later-wins/input.ts +1 -0
- package/fixtures/S-Object/spread-non-object/expect.json +1 -0
- package/fixtures/S-Object/spread-non-object/input.ts +1 -0
- package/fixtures/S-Reject/arrow-as-value/expect.json +1 -0
- package/fixtures/S-Reject/arrow-as-value/input.ts +1 -0
- package/fixtures/S-Reject/call-as-value/expect.json +1 -0
- package/fixtures/S-Reject/call-as-value/input.ts +1 -0
- package/fixtures/S-Template/coercion/expect.json +2 -0
- package/fixtures/S-Template/coercion/input.ts +2 -0
- package/fixtures/S-Template/composite-step-span/expect.json +2 -0
- package/fixtures/S-Template/composite-step-span/input.ts +1 -0
- package/fixtures/S-Template/const-span/expect.json +2 -0
- package/fixtures/S-Template/const-span/input.ts +2 -0
- package/fixtures/S-Template/envelope-span/expect.json +2 -0
- package/fixtures/S-Template/envelope-span/input.ts +2 -0
- package/fixtures/S-Template/nested-construction-span/expect.json +2 -0
- package/fixtures/S-Template/nested-construction-span/input.ts +1 -0
- package/fixtures/S-Template/no-substitution/expect.json +2 -0
- package/fixtures/S-Template/no-substitution/input.ts +1 -0
- package/fixtures/S-Template/unfoldable-span/expect.json +16 -0
- package/fixtures/S-Template/unfoldable-span/input.ts +1 -0
- package/fixtures/S-Unary/not-and-negate/expect.json +1 -0
- package/fixtures/S-Unary/not-and-negate/input.ts +1 -0
- package/fixtures/S-Unclaimed/composite-step/expect.json +3 -0
- package/fixtures/S-Unclaimed/composite-step/input.ts +1 -0
- package/fixtures/S-Unwrap/as-and-parens/expect.json +1 -0
- package/fixtures/S-Unwrap/as-and-parens/input.ts +1 -0
- package/fixtures/UNCOVERED.md +37 -0
- package/package.json +35 -0
- package/spec/VERSION +1 -0
- package/spec/divergence.md +129 -0
- package/spec/grammar.md +277 -0
- package/spec/hosts.md +175 -0
- package/spec/judgments.md +1049 -0
- package/spec/rules.md +102 -0
- package/spec/values.md +247 -0
- package/src/adapter.ts +52 -0
- package/src/adapters/chant.ts +125 -0
- package/src/bundled.ts +27 -0
- package/src/chant-agreement.test.ts +67 -0
- package/src/corpus.test.ts +66 -0
- package/src/corpus.ts +540 -0
- package/src/fixture.ts +117 -0
- package/src/host.ts +105 -0
- package/src/index.ts +6 -0
- package/src/rules.test.ts +50 -0
- package/src/rules.ts +140 -0
- package/src/runner.test.ts +46 -0
- package/src/runner.ts +119 -0
- package/src/version.test.ts +31 -0
package/spec/rules.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Rules over values
|
|
2
|
+
|
|
3
|
+
Normative. The contract a semantic check runs under. It names the input a
|
|
4
|
+
check sees and when it runs, what a check may do and what it reports, and
|
|
5
|
+
who supplies it. The identifiers are `F-Rule-*` (#79). This is neither a
|
|
6
|
+
language for writing checks nor any particular check; those are host
|
|
7
|
+
vocabulary, a lexicon's or a governance tool's. What is specified is the
|
|
8
|
+
guarantee the fold provides and nothing else does. A check sees the values
|
|
9
|
+
of every file in the build before anything is emitted, so the check at the
|
|
10
|
+
keystroke is the check in CI, over real data and across resources.
|
|
11
|
+
|
|
12
|
+
Derived from chant's post-synthesis engine (`packages/core/src/lint/post-synth.ts`),
|
|
13
|
+
its policy layer (`lint/policy.ts`), and its severity configuration
|
|
14
|
+
(`lint/config.ts`), at `chant-v0.71.0`. chant's declarative `rule({…})`
|
|
15
|
+
format matches AST nodes and is the classifier's territory (grammar.md);
|
|
16
|
+
it is not a rule over values and is not covered here.
|
|
17
|
+
|
|
18
|
+
## F-Rule-Input (what a rule sees)
|
|
19
|
+
|
|
20
|
+
A check receives one or both of two inputs and nothing else about the
|
|
21
|
+
build. The first is the folded namespace, every file's `X(f)` after J2 and
|
|
22
|
+
J3 have disposed of every verdict, as final values; in chant this is
|
|
23
|
+
`PostSynthContext.entities`, every declared entity by name (L11.1). The
|
|
24
|
+
second is the artifact, the serializer's output as text and the same output
|
|
25
|
+
parsed once per build into documents (rows `L11.2` and `L11.3`). The name of the
|
|
26
|
+
environment or stack being built may be supplied as well, which is what
|
|
27
|
+
lets an organisational policy vary by environment (L11.4).
|
|
28
|
+
|
|
29
|
+
## F-Rule-Phase (when a rule runs)
|
|
30
|
+
|
|
31
|
+
Checks run after every verdict is final and before anything is applied. A check over the folded namespace is **pre-synthesis**; a check over the
|
|
32
|
+
artifact is **post-synthesis**. One hook may serve both, as chant's
|
|
33
|
+
does: the phase is named by the input the rule reads, not by a separate
|
|
34
|
+
entry point.
|
|
35
|
+
|
|
36
|
+
## F-Rule-Pure (what a rule may do)
|
|
37
|
+
|
|
38
|
+
A check is a function of its input. No execution of project code, no
|
|
39
|
+
environment read beyond the name F-Rule-Input supplies, no network,
|
|
40
|
+
deterministic across runs. This is F-Host-Admission's third clause applied
|
|
41
|
+
to rules, and it is what lets a check run in an editor. Under
|
|
42
|
+
`ι = isolated`, a check the project supplies is itself project code and runs
|
|
43
|
+
where the fallback runs, never in the evaluator's own process (L11.7).
|
|
44
|
+
|
|
45
|
+
## F-Rule-Finding (what a rule reports)
|
|
46
|
+
|
|
47
|
+
A finding carries the rule's identifier, a severity from a closed set
|
|
48
|
+
(`error`, `warning`, `info`), a message, and a **subject**: the name of the
|
|
49
|
+
entity in the namespace or artifact it concerns, or the statement that
|
|
50
|
+
something is missing when there is nothing to attach it to (L11.5). The
|
|
51
|
+
message is not normative (F-Obs-Messages). A source location is part of a
|
|
52
|
+
finding when the implementation has value provenance (F-Obs-Provenance);
|
|
53
|
+
it is not required, because chant's findings name the artifact's entity and
|
|
54
|
+
not a line, and a rule that reaches through the artifact has no line to
|
|
55
|
+
name. A configured severity may override the rule's own (L11.6).
|
|
56
|
+
|
|
57
|
+
## F-Rule-Equivalence (the guarantee)
|
|
58
|
+
|
|
59
|
+
For conforming source, a rule's findings are the same whether the build
|
|
60
|
+
folded the file or ran it. This follows from the objective (judgments.md)
|
|
61
|
+
and from F-Rule-Pure: the inputs are equal on both paths and the rule is a
|
|
62
|
+
function of them. It is what "the check at the keystroke is the check in
|
|
63
|
+
CI" means, stated as a property.
|
|
64
|
+
|
|
65
|
+
## F-Rule-Supply (who supplies a rule)
|
|
66
|
+
|
|
67
|
+
Rules come from the host, as part of a lexicon (F-Host-Interface item 7),
|
|
68
|
+
or from the project, as a policy the build loads by path. A supplied rule is
|
|
69
|
+
identified by its identifier; two suppliers may not claim one (L11.8). The
|
|
70
|
+
profile decides the language: in `data-host` a rule is code in the
|
|
71
|
+
evaluator's own language, since there is no JavaScript to run.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Rationale
|
|
76
|
+
|
|
77
|
+
Non-normative, as in the other rule files: the reasoning behind each rule, keyed by the rules it supports.
|
|
78
|
+
|
|
79
|
+
**F-Rule-Input, F-Rule-Phase.** The fold's user-facing consequence, and the
|
|
80
|
+
one the paper had not stated: a syntax linter sees tokens and a
|
|
81
|
+
configuration language with constraints in the type sees one field; neither
|
|
82
|
+
has the values of every file in the build. chant's post-synthesis checks
|
|
83
|
+
read `ctx.entities` for the values and `ctx.outputs` or `ctx.docs` for the
|
|
84
|
+
artifact, from one hook, which is why the phase is named by the input.
|
|
85
|
+
|
|
86
|
+
**F-Rule-Finding.** The subject is an artifact-side name on purpose. chant's
|
|
87
|
+
own comment says a finding names an identifier from the synthesized output
|
|
88
|
+
such as a CloudFormation logical id and never a source line, and it carries
|
|
89
|
+
a missing-resource form for the case where nothing exists to attach to
|
|
90
|
+
(chant#2113, Snyk's policy-engine archetype).
|
|
91
|
+
Requiring a source line would require value provenance everywhere, which
|
|
92
|
+
`F-Obs-Provenance` deliberately leaves optional.
|
|
93
|
+
|
|
94
|
+
**F-Rule-Pure, F-Rule-Supply.** A project policy is project code. chant
|
|
95
|
+
refuses to load one into its own process while the sandbox is armed and
|
|
96
|
+
runs it in the child instead (chant#1131), which is the isolation mode's
|
|
97
|
+
boundary drawn once more around rules.
|
|
98
|
+
|
|
99
|
+
**Fixtures.** No `F-Rule-*` rule has a fixture yet: the harness has no hook
|
|
100
|
+
that runs a rule and no fixture kind whose expectation is a finding. That
|
|
101
|
+
work is #101; until it lands the family is listed in `fixtures/UNCOVERED.md`
|
|
102
|
+
with that reason.
|
package/spec/values.md
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# The value domain
|
|
2
|
+
|
|
3
|
+
Normative draft (#39). What a fold produces. Rules are `F-Val-*`: the value
|
|
4
|
+
domain is a property of folding's output, not of syntax, so it takes the
|
|
5
|
+
`F-*` prefix (noted for #46). Derived from `FoldedValue` and its cases
|
|
6
|
+
(`fold.ts:116–300`), `FoldableFunction` (`fold.ts:378`), `carriesLiveObject`
|
|
7
|
+
(`fold.ts:411`), `isFoldSymbolicEnvelope`, and `reviveFoldedValue`
|
|
8
|
+
(`fold-import.ts:2643`), at `e4074c17`.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## F-Val-Domain (the closed union)
|
|
13
|
+
|
|
14
|
+
A folded value `v` is exactly one of:
|
|
15
|
+
|
|
16
|
+
| Case | Form | Row |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| scalar | `string` \| `number` \| `boolean` \| `null` \| `undefined` | L4.1 |
|
|
19
|
+
| array | `v[]` | L4.1 |
|
|
20
|
+
| object | `{ [k: string]: v }` with prototype `Object` | L4.1 |
|
|
21
|
+
| attribute reference | `{ __attrRef: { entity, attribute } }` | L3.9 |
|
|
22
|
+
| intrinsic, tag form | `{ __intrinsic, strings: string[], values: v[] }` | L6.3 |
|
|
23
|
+
| intrinsic, call form | `{ __intrinsic, args: v[] }` | L6.3 |
|
|
24
|
+
| helper call | `{ __helper, args: v[] }` | L6.4 |
|
|
25
|
+
| resource | `{ __resource, props, attributes?, args? }` | L6.6 |
|
|
26
|
+
| composite step | `{ __compositeStep, args: v[] }` | L6.5 |
|
|
27
|
+
| symbol | `{ __symbol: string }` | L6.2 |
|
|
28
|
+
|
|
29
|
+
Nothing else. In particular no function is a `v` (F-Val-Callable), and no
|
|
30
|
+
live instance is a `v`, a live instance is what an envelope becomes
|
|
31
|
+
(F-Val-Live).
|
|
32
|
+
|
|
33
|
+
## F-Val-Envelope (six envelopes, recognised by key)
|
|
34
|
+
|
|
35
|
+
A value is an *envelope* iff it is a non-array object carrying one of the keys
|
|
36
|
+
`__attrRef`, `__intrinsic`, `__helper`, `__resource`, `__compositeStep`,
|
|
37
|
+
`__symbol` (`isFoldSymbolicEnvelope`). An envelope is a finished value that
|
|
38
|
+
*denotes* something not yet constructed (R1.1); it is never a thunk, and an
|
|
39
|
+
implementation must not attempt to force it.
|
|
40
|
+
|
|
41
|
+
## F-Val-Fate (what happens to each envelope)
|
|
42
|
+
|
|
43
|
+
Revival (R7.1, `reviveFoldedValue`) walks a folded tree and replaces
|
|
44
|
+
envelopes:
|
|
45
|
+
|
|
46
|
+
| Envelope | Fate |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `__attrRef` | **passes through unchanged** — the serializer accepts the plain envelope (L6.7) |
|
|
49
|
+
| `__intrinsic` (both forms) | revived: the intrinsic function the file's import names is invoked — `Name(strings, ...values)` or `Name(...args)` (L6.3) |
|
|
50
|
+
| `__helper` | revived: the helper the file's import names is invoked with the revived args (L6.4) |
|
|
51
|
+
| `__resource` | revived: the class the file's import names is constructed — from `props`/`attributes`, or by spreading `args` when present (L6.6, F-Val-Arity) |
|
|
52
|
+
| `__compositeStep` | revived: the composite is resolved (J2 F-Call), then `.step` is read off the real result (L6.5) |
|
|
53
|
+
| `__symbol` | revived: the text must match a simple dotted chain; its root resolves through the file's imports and the rest is real property access (L6.2) |
|
|
54
|
+
|
|
55
|
+
In the `data-host` profile (F-Profile-DataHost, judgments.md) none of these
|
|
56
|
+
fates runs: revival is serialization, every envelope is the output, and the
|
|
57
|
+
host's serialization mapping is what turns it into the artifact.
|
|
58
|
+
|
|
59
|
+
**Exactly one envelope survives to serialization: `__attrRef`.** The other
|
|
60
|
+
five must never reach a serializer (R1.2). An implementation that emits a
|
|
61
|
+
`__resource` envelope has produced wrong output, not a placeholder.
|
|
62
|
+
|
|
63
|
+
## F-Val-Position (validity is position-dependent)
|
|
64
|
+
|
|
65
|
+
Revival carries a flag `requireLiveRefs`. It is **true** inside the
|
|
66
|
+
arguments of an `__intrinsic` or `__helper`, the receiving function inspects
|
|
67
|
+
what it is given (`instanceof`, `WeakRef` derefs), and there a `__attrRef`
|
|
68
|
+
envelope is **rejected**, not passed (L6.8): a look-alike plain object would
|
|
69
|
+
produce wrong output rather than absent output. It is **false** for the
|
|
70
|
+
arguments of a `__compositeStep` and for a top-level resource's props (L6.9):
|
|
71
|
+
a composite stores its props rather than inspecting them, and the serializer
|
|
72
|
+
resolves the envelope by name.
|
|
73
|
+
|
|
74
|
+
So the same `v` is valid in one position and a rejection in another. A
|
|
75
|
+
specification of the domain alone does not capture this; the rule is part of
|
|
76
|
+
the domain.
|
|
77
|
+
|
|
78
|
+
## F-Val-Live (liveness)
|
|
79
|
+
|
|
80
|
+
A value *carries a live object* iff it, or anything reachable through plain
|
|
81
|
+
objects and arrays, has a prototype other than `Object`, `Array`, or `null`
|
|
82
|
+
- or is a function (`carriesLiveObject`; R1.4, L4.5). Live objects reached
|
|
83
|
+
through cross-file resolution, an `AttrRef` instance, a `Declarable`, a
|
|
84
|
+
`CompositeInstance`, an `Intrinsic` instance, **pass through revival
|
|
85
|
+
unchanged** (L6.1): the generic walk would rebuild a plain copy and destroy
|
|
86
|
+
the identity J3 exists to preserve. `isIntrinsic` is keyed on a global
|
|
87
|
+
`Symbol.for`, so this holds across separately loaded copies of the core.
|
|
88
|
+
|
|
89
|
+
Liveness is the *entity test* of F-Identity (J3), and is what F-CallLeak
|
|
90
|
+
tests. F-Capture and F-Import test something broader; F-Identity says which
|
|
91
|
+
is normative and what the difference costs.
|
|
92
|
+
|
|
93
|
+
## F-Val-Callable (functions are callable, never values)
|
|
94
|
+
|
|
95
|
+
`FoldableFunction` (R1.3) is a marker for a project-local function J1 may
|
|
96
|
+
*call*. It is **not** a `v`, never appears inside a folded tree, and is
|
|
97
|
+
refused anywhere a value is required: `{ resolver: φ }` does not fold though
|
|
98
|
+
`φ(x)` does (L3.1, L4.4). An eagerly-evaluated lexicon function referenced
|
|
99
|
+
without calling it is refused likewise, with "call it instead" (L3.17).
|
|
100
|
+
|
|
101
|
+
## F-Val-Serializable (the sub-domain that may reach output)
|
|
102
|
+
|
|
103
|
+
A value may reach a serializer iff it is: a scalar; an array or object all of
|
|
104
|
+
whose members may; an `__attrRef` envelope; or a live instance produced by
|
|
105
|
+
revival. Every other envelope, and every callable, may not. The objective's
|
|
106
|
+
"byte-identical serialized output" is stated over this sub-domain.
|
|
107
|
+
|
|
108
|
+
## F-Val-Arity (a resource's constructor arguments)
|
|
109
|
+
|
|
110
|
+
`__resource.props` is the first object-literal argument. When the argument
|
|
111
|
+
list is not `(props)` or `(props, attributes)`, e.g. `new Parameter("String",
|
|
112
|
+
{…})`, `args` is present and **authoritative**: the entity is constructed by
|
|
113
|
+
spreading it, and `props` is reported for readers but never re-passed (L4.2,
|
|
114
|
+
R10.8, R1.5).
|
|
115
|
+
|
|
116
|
+
## F-Val-Undefined (`undefined` in the domain)
|
|
117
|
+
|
|
118
|
+
`undefined` is a scalar of the domain (L4.3). A property whose value is
|
|
119
|
+
`undefined` is **present** in the folded namespace, with that value, and a
|
|
120
|
+
spread copies it like any other own entry; the namespace therefore keeps the
|
|
121
|
+
distinction between an absent key and an `undefined` one, which a consumer
|
|
122
|
+
whose contract is selective-by-omission depends on (#82). Emission is where
|
|
123
|
+
the key is dropped, and an `undefined` array element becomes `null` there,
|
|
124
|
+
for both JSON and YAML, because YAML is round-tripped through the JSON
|
|
125
|
+
emitter (R10.7). For a lexicon that serializes YAML itself, the rule is that
|
|
126
|
+
serializer's own.
|
|
127
|
+
|
|
128
|
+
## F-Val-Symbol-Scope (where `__symbol` may appear)
|
|
129
|
+
|
|
130
|
+
Only inside an intrinsic's interior, a tag's interpolations or a call form's
|
|
131
|
+
arguments (`foldIntrinsicValue`). Anywhere else an unresolved chain is a
|
|
132
|
+
located rejection (F-Reference, J2). A `__symbol` therefore never appears at
|
|
133
|
+
the top of a folded tree.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Rationale
|
|
138
|
+
|
|
139
|
+
Non-normative. The reasoning that motivated each rule, carried over from the retired `requirements.md` (#46). Keyed by the rule(s) each note supports.
|
|
140
|
+
|
|
141
|
+
**F-Val-Domain** *(was R1. The value domain is closed, and its symbolic cases are finished values)*
|
|
142
|
+
|
|
143
|
+
The spec must define what a fold produces. Every other requirement quantifies
|
|
144
|
+
over it, and it is currently defined only by a TypeScript union
|
|
145
|
+
(`FoldedValue`, `fold.ts:116`; L4.1).
|
|
146
|
+
|
|
147
|
+
Nine cases. Six are ordinary JSON, string, number, boolean, null, undefined,
|
|
148
|
+
arrays and plain objects. The rest carry envelopes:
|
|
149
|
+
|
|
150
|
+
| Case | Envelope key | Denotes | Fate (R1.2) |
|
|
151
|
+
|---|---|---|---|
|
|
152
|
+
| `AttrRefValue` | `__attrRef` | an attribute of another entity, resolved at apply | survives |
|
|
153
|
+
| `FoldedIntrinsicTag` | `__intrinsic` | registered intrinsic, tagged-template form | revived |
|
|
154
|
+
| `FoldedIntrinsicCall` | `__intrinsic` | registered intrinsic, call form | revived |
|
|
155
|
+
| `FoldedHelperCall` | `__helper` | registered authoring helper call | revived |
|
|
156
|
+
| `FoldedResource` | `__resource` | a construction, top-level or nested as a value | revived |
|
|
157
|
+
| `FoldedCompositeStepCall` | `__compositeStep` | `<Identifier>(...).step`, member fixed | revived |
|
|
158
|
+
| `SymbolicValue` | `__symbol` | source text preserved inside an intrinsic interior | revived |
|
|
159
|
+
|
|
160
|
+
**F-Val-Envelope** *(was R1.1. Symbolic is not unevaluated)*
|
|
161
|
+
|
|
162
|
+
An `__attrRef` is not a thunk. It is the envelope `AttrRef.prototype.toJSON()`
|
|
163
|
+
produces at runtime, and the serializer accepts it without a live instance. A
|
|
164
|
+
specification that describes these as "unevaluated" invites an implementation
|
|
165
|
+
that tries to force them, which is precisely wrong: the denoted value does not
|
|
166
|
+
exist at build time in either path.
|
|
167
|
+
|
|
168
|
+
**F-Val-Fate, F-Val-Position** *(was R1.2. Exactly one envelope survives to serialization, and its validity is position-dependent)*
|
|
169
|
+
|
|
170
|
+
This corrects the first revision, which had it backwards.
|
|
171
|
+
|
|
172
|
+
`reviveFoldedValue` (L6.1–L6.9) resolves every envelope *except* `__attrRef`
|
|
173
|
+
through the folding file's own imports and invokes the real function or
|
|
174
|
+
constructor: `__intrinsic` in both forms (L6.3), `__helper` (L6.4),
|
|
175
|
+
`__compositeStep` (L6.5), `__resource` (L6.6), `__symbol` via a dotted-chain
|
|
176
|
+
regex (L6.2). None of those may reach a serializer.
|
|
177
|
+
|
|
178
|
+
`__attrRef` passes through unrevived (L6.7), **except** inside an intrinsic's
|
|
179
|
+
or authoring helper's arguments, where it is rejected (L6.8, `requireLiveRefs`)
|
|
180
|
+
because the receiver performs `instanceof` checks and `WeakRef` derefs and a
|
|
181
|
+
look-alike plain object would produce wrong output rather than absent output.
|
|
182
|
+
Composite-step arguments revive with `requireLiveRefs: false` because a
|
|
183
|
+
composite stores its props rather than inspecting them (L6.9).
|
|
184
|
+
|
|
185
|
+
So the spec must say: the same value is valid in one position and invalid in
|
|
186
|
+
another, and which positions are which. A domain definition alone does not
|
|
187
|
+
capture this.
|
|
188
|
+
|
|
189
|
+
**F-Val-Callable** *(was R1.3. Callables are in the domain but are not values)*
|
|
190
|
+
|
|
191
|
+
`FoldableFunction` (L4.4) lets a call to a project-local function fold, and is
|
|
192
|
+
explicitly *not* a `FoldedValue`: it never appears inside a folded tree. A
|
|
193
|
+
function used as a value is refused (L3.1), a `FoldableFunction` reached as a
|
|
194
|
+
bare identifier is refused, and an eagerly-evaluated lexicon function
|
|
195
|
+
referenced without calling it is refused with "call it instead" (L3.17).
|
|
196
|
+
|
|
197
|
+
The specification must therefore define a **serializable sub-domain** and say
|
|
198
|
+
which positions require it.
|
|
199
|
+
|
|
200
|
+
**F-Val-Live** *(was R1.4. Liveness is observable and the spec must say so)*
|
|
201
|
+
|
|
202
|
+
`carriesLiveObject` (L4.5) distinguishes folded data from a live instance by
|
|
203
|
+
prototype, anything other than `Object`/`Array`/`null`, and additionally
|
|
204
|
+
treats `typeof value === "function"` as live. That predicate is what makes the
|
|
205
|
+
identity rules in R4 and R5 statable: without a definition of "this value is a
|
|
206
|
+
live entity rather than plain data," there is nothing for identity to be a
|
|
207
|
+
property of.
|
|
208
|
+
|
|
209
|
+
**F-Val-Arity** *(was R1.5. Constructor arity is contractual)*
|
|
210
|
+
|
|
211
|
+
`FoldedResource.args` (L4.2) is present when the argument list is not the
|
|
212
|
+
classic `(props)` / `(props, attributes)` shape, and is then authoritative:
|
|
213
|
+
the entity is constructed by spreading it. `props` is a view. The
|
|
214
|
+
`undefined` case in the union (L4.3) has no rule today. Both are #42's.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
**F-Val-Fate** *(was R7.1. Envelope, then revive)*
|
|
219
|
+
|
|
220
|
+
The default for everything R1's table marks *revived*: `fold()` executes
|
|
221
|
+
nothing and records what was named; the bridge resolves the name through the
|
|
222
|
+
folding file's imports and invokes it (R1.2, R2).
|
|
223
|
+
|
|
224
|
+
**F-Val-Undefined** *(was R10.7, `undefined` is absent, not `null`, in a property; and is `null` in an array)*
|
|
225
|
+
|
|
226
|
+
The domain admits `undefined` (L4.3). The serializer walker passes it through
|
|
227
|
+
unchanged and keeps the key (`serializer-walker.ts:33`, `:117`); the drop
|
|
228
|
+
happens at emission, and, verified, it happens for both formats because
|
|
229
|
+
YAML is produced by round-tripping the sorted JSON (`build.ts:743–746`), so
|
|
230
|
+
`JSON.stringify` has already removed an `undefined`-valued key and turned an
|
|
231
|
+
`undefined` array element into `null` before any YAML exists. That is what
|
|
232
|
+
makes chant's build-parameters documentation true ("dropped from the output
|
|
233
|
+
in both JSON and YAML rather than shipped as `null`"). Both facts must be
|
|
234
|
+
stated because they are the difference between "absent" and "null", which
|
|
235
|
+
platforms treat differently, and for the six YAML-native lexicons above the
|
|
236
|
+
walker's `undefined` reaches *their* emitter directly, so the rule there is
|
|
237
|
+
each serializer's, not `JSON.stringify`'s.
|
|
238
|
+
|
|
239
|
+
**F-Val-Arity** *(was R10.8. Constructor arity)*
|
|
240
|
+
|
|
241
|
+
`FoldedResource.args` (L4.2, R1.5): present when the argument list is not
|
|
242
|
+
`(props)` or `(props, attributes)`, authoritative when present, the entity
|
|
243
|
+
constructed by spreading it; `props` is a view, never re-passed. An
|
|
244
|
+
implementation that assumed the props object is always first would construct
|
|
245
|
+
`new Parameter("String", {...})` wrongly.
|
|
246
|
+
|
|
247
|
+
---
|
package/src/adapter.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The adapter an implementation under test provides. Deliberately narrow so a
|
|
3
|
+
* foreign implementation (chant, or one in another language behind a shim)
|
|
4
|
+
* can satisfy it without exposing internals: source in, verdict out.
|
|
5
|
+
*/
|
|
6
|
+
import type { ConformanceHost } from "./host.js";
|
|
7
|
+
|
|
8
|
+
export type ShapeResult =
|
|
9
|
+
| { accepted: true }
|
|
10
|
+
| { accepted: false; rule?: string; line: number; column: number; message: string }
|
|
11
|
+
| "unavailable";
|
|
12
|
+
export type FoldResult =
|
|
13
|
+
| { ok: true; value: unknown }
|
|
14
|
+
| { ok: false; rule?: string; line: number; column: number; message: string };
|
|
15
|
+
/** A whole-build verdict (J2 + J3). `rule` and `reason` are diagnostic; only `kind` is compared. */
|
|
16
|
+
export type ProjectVerdict =
|
|
17
|
+
| { kind: "fold"; exports: Record<string, unknown> }
|
|
18
|
+
| { kind: "run"; rule?: string; reason: string };
|
|
19
|
+
export interface ProjectResult {
|
|
20
|
+
verdicts: Record<string, ProjectVerdict>;
|
|
21
|
+
/** J2's proposal, before J3 disposed of it. Omitted by an implementation that cannot separate the two phases. */
|
|
22
|
+
tentative?: Record<string, "fold" | "run">;
|
|
23
|
+
/** For a file J3 tainted, the file whose taint reached it. */
|
|
24
|
+
taintedBy?: Record<string, string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ConformanceAdapter {
|
|
28
|
+
readonly name: string;
|
|
29
|
+
/**
|
|
30
|
+
* The specification version this implementation declares it implements,
|
|
31
|
+
* as `spec/VERSION` spells it (`"1.0"`), or `"undeclared"`. Compared, never
|
|
32
|
+
* assumed: the suite checks the reference's declaration against `VERSION`,
|
|
33
|
+
* and reports an implementation that declares nothing as such (#18).
|
|
34
|
+
*/
|
|
35
|
+
readonly specVersion: string;
|
|
36
|
+
/** S-* verdict on the initializer of `exportName`. "unavailable" if the implementation exposes no shape classifier. */
|
|
37
|
+
shape(source: string, exportName: string): ShapeResult;
|
|
38
|
+
/** F-* verdict: fold the initializer of `exportName` to a JSON-comparable value, or a located rejection. */
|
|
39
|
+
foldExport(source: string, exportName: string): FoldResult;
|
|
40
|
+
/**
|
|
41
|
+
* J2 + J3 over a whole build: every file's final verdict. Optional, and
|
|
42
|
+
* "unavailable" when the implementation cannot answer — no whole-build
|
|
43
|
+
* entry at all, or a host it has no way to install. Such a fixture is
|
|
44
|
+
* reported skipped rather than silently passing.
|
|
45
|
+
*
|
|
46
|
+
* May be async: a real implementation resolves modules from a filesystem.
|
|
47
|
+
*/
|
|
48
|
+
foldProject?(
|
|
49
|
+
files: Map<string, string>,
|
|
50
|
+
host?: ConformanceHost,
|
|
51
|
+
): ProjectResult | "unavailable" | Promise<ProjectResult | "unavailable">;
|
|
52
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #11 — chant as an implementation under test, through its PUBLIC entry only:
|
|
3
|
+
* `fold`, `collectConsts`, `FoldError`, and — since chant-v0.64.0 (chant#2362)
|
|
4
|
+
* — `findSubsetViolation` for the shape half. If an older chant is pinned the
|
|
5
|
+
* adapter reports shape "unavailable" rather than guessing.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
import * as ts from "typescript";
|
|
12
|
+
import * as chant from "@intentius/chant";
|
|
13
|
+
import type { ConformanceAdapter, ProjectResult, ProjectVerdict } from "../adapter.js";
|
|
14
|
+
|
|
15
|
+
function exportInitializer(sf: ts.SourceFile, name: string): ts.Expression | undefined {
|
|
16
|
+
for (const st of sf.statements) {
|
|
17
|
+
if (name === "default" && ts.isExportAssignment(st) && !st.isExportEquals) return st.expression;
|
|
18
|
+
if (!ts.isVariableStatement(st) || !st.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) continue;
|
|
19
|
+
for (const d of st.declarationList.declarations) if (ts.isIdentifier(d.name) && d.name.text === name) return d.initializer;
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
const parse = (src: string) => ts.createSourceFile("fixture.ts", src, ts.ScriptTarget.Latest, true);
|
|
24
|
+
function chantVersion(): string {
|
|
25
|
+
const declared = (chant as unknown as { VERSION?: string }).VERSION;
|
|
26
|
+
if (typeof declared === "string") return declared;
|
|
27
|
+
// The package does not export its own manifest, so walk up from the resolved
|
|
28
|
+
// entry to the nearest one.
|
|
29
|
+
let dir = dirname(createRequire(import.meta.url).resolve("@intentius/chant"));
|
|
30
|
+
for (;;) {
|
|
31
|
+
const candidate = join(dir, "package.json");
|
|
32
|
+
if (existsSync(candidate)) {
|
|
33
|
+
const version = (JSON.parse(readFileSync(candidate, "utf8")) as { version?: string }).version;
|
|
34
|
+
if (typeof version === "string") return version;
|
|
35
|
+
}
|
|
36
|
+
const up = dirname(dir);
|
|
37
|
+
if (up === dir) throw new Error("cannot determine the pinned chant version");
|
|
38
|
+
dir = up;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const shapeFn = (chant as unknown as { findSubsetViolation?: (n: ts.Node) => { node: ts.Node; ruleId: string; message: string } | undefined }).findSubsetViolation;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The whole-build entry, chant-v0.70.1+ (chant#2408). Absent on an older pin,
|
|
46
|
+
* in which case the project fixtures report "unavailable" rather than passing
|
|
47
|
+
* vacuously.
|
|
48
|
+
*/
|
|
49
|
+
type ChantVerdict = {
|
|
50
|
+
verdict: "fold" | "run";
|
|
51
|
+
tentative: "fold" | "run";
|
|
52
|
+
reason?: string;
|
|
53
|
+
taintedBy?: { from: string; kind: "importer" | "capture" };
|
|
54
|
+
exports?: ReadonlyMap<string, unknown>;
|
|
55
|
+
};
|
|
56
|
+
const projectFn = (
|
|
57
|
+
chant as unknown as {
|
|
58
|
+
foldProject?: (files: readonly string[], intrinsics?: readonly unknown[]) => Promise<Map<string, ChantVerdict>>;
|
|
59
|
+
}
|
|
60
|
+
).foldProject;
|
|
61
|
+
|
|
62
|
+
/** chant resolves modules from disk, so a fixture's sources are written out and the paths handed over. */
|
|
63
|
+
async function foldOnDisk(files: Map<string, string>): Promise<ProjectResult> {
|
|
64
|
+
const root = mkdtempSync(join(tmpdir(), "tsad-conformance-"));
|
|
65
|
+
try {
|
|
66
|
+
const paths: string[] = [];
|
|
67
|
+
for (const [rel, source] of files) {
|
|
68
|
+
const abs = join(root, rel);
|
|
69
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
70
|
+
writeFileSync(abs, source, "utf8");
|
|
71
|
+
paths.push(abs);
|
|
72
|
+
}
|
|
73
|
+
const verdicts = await projectFn!(paths, []);
|
|
74
|
+
const out: ProjectResult = { verdicts: {}, tentative: {}, taintedBy: {} };
|
|
75
|
+
const relOf = (abs: string) => abs.slice(root.length + 1).split(/[\\/]/).join("/");
|
|
76
|
+
for (const [abs, v] of verdicts) {
|
|
77
|
+
const key = relOf(abs);
|
|
78
|
+
out.verdicts[key] =
|
|
79
|
+
v.verdict === "fold"
|
|
80
|
+
? ({ kind: "fold", exports: Object.fromEntries(v.exports ?? new Map()) } as ProjectVerdict)
|
|
81
|
+
: ({ kind: "run", reason: v.reason ?? "tainted" } as ProjectVerdict);
|
|
82
|
+
out.tentative![key] = v.tentative;
|
|
83
|
+
if (v.taintedBy) out.taintedBy![key] = relOf(v.taintedBy.from);
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
} finally {
|
|
87
|
+
rmSync(root, { recursive: true, force: true });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const chantAdapter: ConformanceAdapter = {
|
|
92
|
+
// Read from the installed package, never a literal: a hardcoded fallback
|
|
93
|
+
// silently misreports the pin, and this name is what the paper's measurement
|
|
94
|
+
// table cites.
|
|
95
|
+
name: `chant@${chantVersion()}`,
|
|
96
|
+
// chant declares the specification version it implements in its own tree
|
|
97
|
+
// once it does so at all; a pin that exports none is reported as undeclared
|
|
98
|
+
// rather than assumed current (#18).
|
|
99
|
+
specVersion: (chant as unknown as { SPEC_VERSION?: string }).SPEC_VERSION ?? "undeclared",
|
|
100
|
+
shape(source, exportName) {
|
|
101
|
+
if (!shapeFn) return "unavailable";
|
|
102
|
+
const sf = parse(source); const init = exportInitializer(sf, exportName);
|
|
103
|
+
if (!init) return { accepted: false, line: 1, column: 1, message: `no export named ${exportName}` };
|
|
104
|
+
const v = shapeFn(init); if (!v) return { accepted: true };
|
|
105
|
+
const { line, character } = sf.getLineAndCharacterOfPosition(v.node.getStart());
|
|
106
|
+
return { accepted: false, rule: v.ruleId, line: line + 1, column: character + 1, message: v.message };
|
|
107
|
+
},
|
|
108
|
+
async foldProject(files, host) {
|
|
109
|
+
if (!projectFn) return "unavailable";
|
|
110
|
+
// A named host's entity classes come from a package chant cannot resolve,
|
|
111
|
+
// and its entry takes an intrinsic registry rather than a whole host, so a
|
|
112
|
+
// host-dependent fixture is not answerable here.
|
|
113
|
+
if (host) return "unavailable";
|
|
114
|
+
return foldOnDisk(files);
|
|
115
|
+
},
|
|
116
|
+
foldExport(source, exportName) {
|
|
117
|
+
const sf = parse(source); const init = exportInitializer(sf, exportName);
|
|
118
|
+
if (!init) return { ok: false, line: 1, column: 1, message: `no export named ${exportName}` };
|
|
119
|
+
try { return { ok: true, value: chant.fold(init, chant.collectConsts(sf), [], undefined) }; }
|
|
120
|
+
catch (e) {
|
|
121
|
+
if (e instanceof chant.FoldError) return { ok: false, rule: e.ruleId, line: e.line, column: e.column, message: e.message };
|
|
122
|
+
throw e;
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
};
|
package/src/bundled.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the published package keeps the fixtures. `spec/fixtures/` is copied
|
|
3
|
+
* in at build time (see `package.json`'s build script), so an implementation
|
|
4
|
+
* that installed this package from npm has the same fixtures the repository
|
|
5
|
+
* gates on, at the version the package declares.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
11
|
+
/** The bundled rule files and `VERSION`, or the repository's own `spec/` when running from source. */
|
|
12
|
+
export function bundledSpecDir(): string {
|
|
13
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
for (const candidate of [join(here, "..", "spec"), join(here, "..", "..", "..", "spec")]) {
|
|
15
|
+
if (existsSync(join(candidate, "VERSION"))) return candidate;
|
|
16
|
+
}
|
|
17
|
+
throw new Error("no spec directory: the package was built without one, and this is not the repository");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The bundled fixtures directory, or the repository's own when running from source. */
|
|
21
|
+
export function bundledFixturesDir(): string {
|
|
22
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
for (const candidate of [join(here, "..", "fixtures"), join(here, "..", "..", "..", "spec", "fixtures")]) {
|
|
24
|
+
if (existsSync(join(candidate, "UNCOVERED.md"))) return candidate;
|
|
25
|
+
}
|
|
26
|
+
throw new Error("no fixtures directory: the package was built without one, and this is not the repository");
|
|
27
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** #11 — chant must pass the fixtures, and must agree with the reference implementation on every one. */
|
|
2
|
+
import { describe, test, expect } from "vitest";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { loadFixtures, runFixtures, runProjectFixture, compareAdapters, expressionFixtures, projectFixtures } from "./index";
|
|
6
|
+
import { chantAdapter } from "./adapters/chant";
|
|
7
|
+
import { referenceAdapter } from "@intentius/tsad-reference";
|
|
8
|
+
|
|
9
|
+
// chant implements the full profile; a fixture tagged for data-host only
|
|
10
|
+
// (S-ExportDefault, which full keeps under S-Disqualify until chant admits it)
|
|
11
|
+
// is not chant's to answer.
|
|
12
|
+
const all = loadFixtures(join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "spec", "fixtures")).filter((f) => f.profiles.includes("full"));
|
|
13
|
+
// chant's classifier rejects every project-local call the build folds
|
|
14
|
+
// (chant#2435), the direction F-Direction forbids; spec 1.2's S-CallLocal
|
|
15
|
+
// names the form. Fixtures citing it are held out of the shape comparison,
|
|
16
|
+
// and only that: their fold half is still compared below.
|
|
17
|
+
const knownShapeDivergence = new Set(["S-CallLocal"]);
|
|
18
|
+
const heldOut = all.filter((f) => f.rules.some((r) => knownShapeDivergence.has(r)));
|
|
19
|
+
const fixtures = all.filter((f) => !heldOut.includes(f));
|
|
20
|
+
|
|
21
|
+
describe("chant cross-check (#11)", () => {
|
|
22
|
+
test("chant passes every fixture through its public fold API", async () => {
|
|
23
|
+
const failed = (await runFixtures(chantAdapter, fixtures)).filter((r) => !r.pass).map((r) => `${r.fixture}: ${r.failures.join("; ")}`);
|
|
24
|
+
expect(failed, failed.join("\n")).toEqual([]);
|
|
25
|
+
});
|
|
26
|
+
test("chant and the reference implementation agree on every fixture", async () => {
|
|
27
|
+
const dis = await compareAdapters(referenceAdapter, chantAdapter, fixtures);
|
|
28
|
+
expect(dis, dis.join("\n")).toEqual([]);
|
|
29
|
+
});
|
|
30
|
+
test("the whole-build fixtures reach chant, or say why not (#62)", async () => {
|
|
31
|
+
// Two reasons a project fixture can be skipped, and both must be visible.
|
|
32
|
+
// An older pin has no whole-build entry at all (chant#2408); a fixture
|
|
33
|
+
// naming a host asks for entity classes from a package chant cannot
|
|
34
|
+
// resolve. Anything else has to be answered.
|
|
35
|
+
const projects = projectFixtures(fixtures);
|
|
36
|
+
expect(projects.length).toBeGreaterThan(0);
|
|
37
|
+
const reports = await Promise.all(projects.map((f) => runProjectFixture(chantAdapter, f)));
|
|
38
|
+
const skipped = reports.filter((r) => r.skipped).map((r) => r.fixture);
|
|
39
|
+
const hosted = new Set(projects.filter((f) => f.host).map((f) => f.id));
|
|
40
|
+
const unexpected = skipped.filter((id) => !hosted.has(id));
|
|
41
|
+
expect(unexpected, `skipped without a host to explain it:\n${unexpected.join("\n")}`).toEqual([]);
|
|
42
|
+
// The pin carries the entry, so "unavailable" everywhere would mean the
|
|
43
|
+
// comparison silently stopped happening. Asserted, not assumed.
|
|
44
|
+
expect(skipped.length, "every hostless whole-build fixture must reach chant").toBeLessThan(projects.length);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("chant and the reference agree on every whole-build fixture chant can answer (#62)", async () => {
|
|
48
|
+
const dis = await compareAdapters(referenceAdapter, chantAdapter, projectFixtures(fixtures));
|
|
49
|
+
expect(dis, dis.join("\n")).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
test("the held-out fixtures still agree on the fold half, and the hold-out is not empty (chant#2435)", () => {
|
|
52
|
+
expect(heldOut.length).toBeGreaterThan(0);
|
|
53
|
+
for (const f of expressionFixtures(heldOut)) {
|
|
54
|
+
const a = referenceAdapter.foldExport(f.input, f.exportName), b = chantAdapter.foldExport(f.input, f.exportName);
|
|
55
|
+
expect(a.ok, `${f.id}: reference ${a.ok ? "folds" : "runs"}, chant ${b.ok ? "folds" : "runs"}`).toBe(b.ok);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
test("chant's shape classifier is available (chant-v0.64.0+, chant#2362) and agrees on every fixture", async () => {
|
|
59
|
+
// The pinned chant carries the export, so "unavailable" would mean the adapter
|
|
60
|
+
// silently stopped comparing the shape half — a real regression, asserted.
|
|
61
|
+
for (const f of expressionFixtures(fixtures)) {
|
|
62
|
+
const s = chantAdapter.shape(f.input, f.exportName);
|
|
63
|
+
expect(s, `${f.id}: chant shape classifier unavailable`).not.toBe("unavailable");
|
|
64
|
+
if (s !== "unavailable") expect(s.accepted, `${f.id}: chant shape ${s.accepted ? "accept" : "reject"} ≠ expected ${f.shape}`).toBe(f.shape === "accept");
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|