@intentius/chant 0.72.4 → 0.72.5
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/declarable.d.ts +46 -1
- package/dist/declarable.d.ts.map +1 -1
- package/dist/discovery/sandbox/driver.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +15 -3
- package/dist/fold/subset.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/declarable-host-marker.test.ts +88 -0
- package/src/declarable.ts +72 -7
- package/src/discovery/sandbox/driver.ts +25 -0
- package/src/discovery/sandbox/fork-diagnostic.test.ts +9 -1
- package/src/fold/subset.ts +15 -3
package/dist/declarable.d.ts
CHANGED
|
@@ -40,7 +40,52 @@ export interface CoreOutput extends Declarable {
|
|
|
40
40
|
readonly value: unknown;
|
|
41
41
|
}
|
|
42
42
|
/**
|
|
43
|
-
* Type guard to check if a value is a Declarable
|
|
43
|
+
* Type guard to check if a value is a Declarable.
|
|
44
|
+
*
|
|
45
|
+
* chant#2444 — accepts an entity a HOST built, not only one chant built.
|
|
46
|
+
*
|
|
47
|
+
* `F-Host-Interface` item 1 says an entity carries a non-enumerable declarable
|
|
48
|
+
* marker, and a host supplies its own: the specification's conformance host
|
|
49
|
+
* marks with `Symbol.for("tsad.conformance.declarable")`. Testing identity
|
|
50
|
+
* against chant's symbol alone refused those for not being chant's rather than
|
|
51
|
+
* for being malformed, which is what chant#2442 hit inside composite member
|
|
52
|
+
* validation and what would otherwise keep biting in the entity tallies and the
|
|
53
|
+
* revival pass-through.
|
|
54
|
+
*
|
|
55
|
+
* ## Why NOT the reference implementation's reading
|
|
56
|
+
*
|
|
57
|
+
* The reference reads L6.1 as "any own symbol or non-enumerable own property",
|
|
58
|
+
* which is right in its domain and **unsafe in chant's**. Two measurements say
|
|
59
|
+
* so, and both were taken rather than assumed:
|
|
60
|
+
*
|
|
61
|
+
* - A real chant entity's non-enumerable own NAMES are `lexicon`,
|
|
62
|
+
* `entityType`, `kind`, `props`, `attributes`, `Ref`. "Any non-enumerable
|
|
63
|
+
* own property" is therefore close to "any chant object at all".
|
|
64
|
+
* - chant has SEVEN marker symbols, not one — `chant.intrinsic`,
|
|
65
|
+
* `chant.composite`, `chant.stackOutput`, `chant.lexiconOutput`,
|
|
66
|
+
* `chant.effect-receipt`, `chant.secret-declaration`, `chant.childProject`
|
|
67
|
+
* — each a non-enumerable own symbol set to `true`. Under "any own symbol"
|
|
68
|
+
* an Intrinsic and a StackOutput both classify as Declarable, which is a
|
|
69
|
+
* worse bug than the one being fixed: they would enter the entity tallies.
|
|
70
|
+
*
|
|
71
|
+
* The reference can read any symbol because its domain has one. chant
|
|
72
|
+
* distinguishes seven kinds BY symbol identity, so it needs the marker's name.
|
|
73
|
+
* Hence the suffix: the marker must say `declarable`, whoever owns it.
|
|
74
|
+
*
|
|
75
|
+
* ## What the suffix cannot do
|
|
76
|
+
*
|
|
77
|
+
* It is a convention, not a registry. A host that marks with, say,
|
|
78
|
+
* `Symbol.for("acme.entity")` carries a perfectly good non-enumerable marker
|
|
79
|
+
* and is refused here, because nothing in this function knows that package is
|
|
80
|
+
* one the caller named. The context that would settle it —
|
|
81
|
+
* `FoldProjectOptions.lexiconPackages`, chant#2438 — lives in the fold path and
|
|
82
|
+
* does not reach a type guard called from a dozen places.
|
|
83
|
+
*
|
|
84
|
+
* That is a deliberate trade, not an oversight: a convention that refuses an
|
|
85
|
+
* unknown-but-valid marker costs a host one symbol name, while a rule loose
|
|
86
|
+
* enough to accept any marker miscounts chant's own seven kinds on every build.
|
|
87
|
+
* If a real host ever needs the other side of it, the fix is to thread the
|
|
88
|
+
* named packages through rather than to widen the test.
|
|
44
89
|
*/
|
|
45
90
|
export declare function isDeclarable(value: unknown): value is Declarable;
|
|
46
91
|
/**
|
package/dist/declarable.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"declarable.d.ts","sourceRoot":"","sources":["../src/declarable.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,eAAO,MAAM,iBAAiB,eAAiC,CAAC;AAEhE;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,UAAU,GAAG,UAAU,GAAG,QAAQ,CAAC;IACnD,QAAQ,CAAC,CAAC,iBAAiB,CAAC,EAAE,IAAI,CAAC;CACpC;AAED;;;;;GAKG;AACH,MAAM,WAAW,kBAAmB,SAAQ,UAAU;IACpD,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,UAAU,GAAG,KAAK,IAAI,kBAAkB,CAEnF;AAED;;GAEG;AACH,MAAM,WAAW,aAAc,SAAQ,UAAU;IAC/C,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,WAAW,UAAW,SAAQ,UAAU;IAC5C,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;
|
|
1
|
+
{"version":3,"file":"declarable.d.ts","sourceRoot":"","sources":["../src/declarable.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,eAAO,MAAM,iBAAiB,eAAiC,CAAC;AAEhE;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,UAAU,GAAG,UAAU,GAAG,QAAQ,CAAC;IACnD,QAAQ,CAAC,CAAC,iBAAiB,CAAC,EAAE,IAAI,CAAC;CACpC;AAED;;;;;GAKG;AACH,MAAM,WAAW,kBAAmB,SAAQ,UAAU;IACpD,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,UAAU,GAAG,KAAK,IAAI,kBAAkB,CAEnF;AAED;;GAEG;AACH,MAAM,WAAW,aAAc,SAAQ,UAAU;IAC/C,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,WAAW,UAAW,SAAQ,UAAU;IAC5C,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAWD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,UAAU,CAKhE;AAeD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAE/D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../../../src/discovery/sandbox/driver.ts"],"names":[],"mappings":"AA4DA,MAAM,WAAW,qBAAqB;IACpC,qIAAqI;IACrI,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,oNAAoN;IACpN,SAAS,EAAE,MAAM,CAAC;CACnB;AAOD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../../../src/discovery/sandbox/driver.ts"],"names":[],"mappings":"AA4DA,MAAM,WAAW,qBAAqB;IACpC,qIAAqI;IACrI,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,oNAAoN;IACpN,SAAS,EAAE,MAAM,CAAC;CACnB;AAOD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,MAAM,CAsH3E;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CA6CrE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,0BAA0B,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAmGjF"}
|
package/dist/fold/subset.d.ts
CHANGED
|
@@ -207,13 +207,25 @@ import { type IntrinsicDef } from "../lexicon.js";
|
|
|
207
207
|
* binding whatever an invoked factory returns is what chant never checked
|
|
208
208
|
* (`L8.19`), and a declarator reaching a call through a const alias is what
|
|
209
209
|
* chant resolves while refusing a call nested elsewhere (`L2.18`).
|
|
210
|
+
* - **1.7** put `F-Eval-CallLocal` before the two registered call shapes, so a
|
|
211
|
+
* call through a name the project bound is the project function's call
|
|
212
|
+
* whatever the registry says. That is what chant's whole-build fold already
|
|
213
|
+
* did, and the reason the specification moved rather than chant: probes with
|
|
214
|
+
* chant's own `output` showed the binding-first order living in
|
|
215
|
+
* `resolveCallExpression`, while `L2.11` described the expression
|
|
216
|
+
* classifier. The two were never required to share an order.
|
|
217
|
+
* - **1.8** added `ι = executing`, and chant carries it as of `0.72.3`
|
|
218
|
+
* (#2455) — `FoldProjectOptions.executing`, mutually exclusive with
|
|
219
|
+
* `sandbox`. `open` stays the default and stays strict (#2453).
|
|
210
220
|
*
|
|
211
221
|
* `scripts/check-docs-citations.ts` refuses to run when this constant and the
|
|
212
222
|
* pinned specification disagree, so `.github/workflows/docs-check.yml` pins the
|
|
213
|
-
* `spec-1.
|
|
214
|
-
* is what stops the docs being gated against a rule set nobody writes against
|
|
223
|
+
* `spec-1.8` commit and the two move together. That coupling is deliberate: it
|
|
224
|
+
* is what stops the docs being gated against a rule set nobody writes against
|
|
225
|
+
* — which is exactly what happened while this sat at `1.0` through six
|
|
226
|
+
* versions, passing because both sides were equally stale.
|
|
215
227
|
*/
|
|
216
|
-
export declare const SPEC_VERSION = "1.
|
|
228
|
+
export declare const SPEC_VERSION = "1.8";
|
|
217
229
|
/** The two EVL rule ids a shape violation can be attributed to. */
|
|
218
230
|
export type SubsetRuleId = "EVL001" | "EVL003";
|
|
219
231
|
/** A located, shape-level rejection: the offending node, the EVL rule id it maps to, and a message. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subset.d.ts","sourceRoot":"","sources":["../../src/fold/subset.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,YAAY,CAAC;AAEjC,OAAO,EAAiD,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAE9F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwKG;AAEH
|
|
1
|
+
{"version":3,"file":"subset.d.ts","sourceRoot":"","sources":["../../src/fold/subset.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,YAAY,CAAC;AAEjC,OAAO,EAAiD,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAE9F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwKG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,eAAO,MAAM,YAAY,QAAQ,CAAC;AAElC,mEAAmE;AACnE,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE/C,uGAAuG;AACvG,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC;IACd,MAAM,EAAE,YAAY,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;CACjB;AAMD,0FAA0F;AAC1F,eAAO,MAAM,0BAA0B,EAAE,WAAW,CAAC,EAAE,CAAC,UAAU,CAchE,CAAC;AAEH,oFAAoF;AACpF,eAAO,MAAM,yBAAyB,EAAE,WAAW,CAAC,EAAE,CAAC,UAAU,CAG/D,CAAC;AAEH,gHAAgH;AAChH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,EAAE,CAAC,YAAY,GACpB,IAAI,IAAI,EAAE,CAAC,UAAU,GAAG,EAAE,CAAC,aAAa,GAAG,EAAE,CAAC,cAAc,CAE9D;AAED,6GAA6G;AAC7G,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,EAAE,CAAC,UAAU,GAAG,IAAI,IAAI,EAAE,CAAC,aAAa,GAAG,EAAE,CAAC,cAAc,CAErG;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,SAAS,SAAK,GAAG,MAAM,CAGnE;AAMD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,GAAG,WAAW,CAAC,MAAM,CAAC,CAoDpF;AAgCD,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,EAAE,CAAC,YAAY,GAAG,MAAM,CAEzE;AAED,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,GAAG,MAAM,CAE1E;AAED,eAAO,MAAM,iCAAiC,8BAA8B,CAAC;AAE7E,eAAO,MAAM,yBAAyB,sBAAsB,CAAC;AAE7D,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,GAAG,MAAM,CAEtE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,EAAE,CAAC,cAAc,GAAG,MAAM,CAErE;AAED,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,GAAG,MAAM,CAElE;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,EAAE,CAAC,wBAAwB,EACjC,UAAU,CAAC,EAAE,SAAS,YAAY,EAAE,GACnC,eAAe,GAAG,SAAS,CAc7B;AAWD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,UAAU,CAAC,EAAE,SAAS,YAAY,EAAE,GACnC,eAAe,GAAG,SAAS,CAkO7B"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { isDeclarable, DECLARABLE_MARKER } from "./declarable";
|
|
3
|
+
import { INTRINSIC_MARKER } from "./intrinsic";
|
|
4
|
+
import { STACK_OUTPUT_MARKER } from "./stack-output";
|
|
5
|
+
import { COMPOSITE_MARKER } from "./composite";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* chant#2444 — `isDeclarable` accepts an entity a HOST built.
|
|
9
|
+
*
|
|
10
|
+
* `F-Host-Interface` item 1 says an entity carries a non-enumerable declarable
|
|
11
|
+
* marker, and a host supplies its own. Testing identity against chant's symbol
|
|
12
|
+
* alone refused those for not being chant's rather than for being malformed —
|
|
13
|
+
* which is what chant#2442 hit inside composite member validation, and what
|
|
14
|
+
* would otherwise keep biting in the entity tallies and the revival
|
|
15
|
+
* pass-through, where it fails silently rather than loudly.
|
|
16
|
+
*
|
|
17
|
+
* The interesting half of this test file is the second describe block. The
|
|
18
|
+
* reference implementation reads the same rule as "any own symbol or
|
|
19
|
+
* non-enumerable own property", which is right for a domain with one marker and
|
|
20
|
+
* wrong for chant, which has seven. Those cases pin the narrowing.
|
|
21
|
+
*/
|
|
22
|
+
function marked(symbol: symbol, value: unknown = true): object {
|
|
23
|
+
const o = {};
|
|
24
|
+
Object.defineProperty(o, symbol, { value, enumerable: false });
|
|
25
|
+
return o;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("isDeclarable accepts a host's marker (chant#2444)", () => {
|
|
29
|
+
test("chant's own marker, which is every entity a chant build makes", () => {
|
|
30
|
+
expect(isDeclarable(marked(DECLARABLE_MARKER))).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("the specification host's marker", () => {
|
|
34
|
+
// The exact symbol `@tsad/shapes` uses. This is the case chant#2442 hit.
|
|
35
|
+
expect(isDeclarable(marked(Symbol.for("tsad.conformance.declarable")))).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("any implementation's, as long as the marker says declarable", () => {
|
|
39
|
+
expect(isDeclarable(marked(Symbol.for("some.other.tool.declarable")))).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("a plain object is not one", () => {
|
|
43
|
+
expect(isDeclarable({})).toBe(false);
|
|
44
|
+
expect(isDeclarable({ entityType: "Bucket", props: {} })).toBe(false);
|
|
45
|
+
expect(isDeclarable(null)).toBe(false);
|
|
46
|
+
expect(isDeclarable("Bucket")).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("the narrowing, and why the reference's reading is unsafe here", () => {
|
|
51
|
+
test("chant's OTHER markers are not declarable markers", () => {
|
|
52
|
+
// The measurement that decided the implementation. Under "any own symbol",
|
|
53
|
+
// every one of these classifies as an entity and enters the tallies — a
|
|
54
|
+
// worse bug than the one chant#2444 fixes.
|
|
55
|
+
expect(isDeclarable(marked(INTRINSIC_MARKER))).toBe(false);
|
|
56
|
+
expect(isDeclarable(marked(STACK_OUTPUT_MARKER))).toBe(false);
|
|
57
|
+
expect(isDeclarable(marked(COMPOSITE_MARKER))).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("a non-enumerable own NAME is not a marker either", () => {
|
|
61
|
+
// A real chant entity's non-enumerable own names are `lexicon`,
|
|
62
|
+
// `entityType`, `kind`, `props`, `attributes` and `Ref`. Reading "any
|
|
63
|
+
// non-enumerable own property" as the marker is close to "any chant object".
|
|
64
|
+
const o = {};
|
|
65
|
+
Object.defineProperty(o, "props", { value: {}, enumerable: false });
|
|
66
|
+
Object.defineProperty(o, "entityType", { value: "Bucket", enumerable: false });
|
|
67
|
+
expect(isDeclarable(o)).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("an ENUMERABLE declarable symbol is refused", () => {
|
|
71
|
+
// F-Host-Interface item 1 says non-enumerable, and it matters: an
|
|
72
|
+
// enumerable marker travels through a spread and would reach an artifact.
|
|
73
|
+
const o = {};
|
|
74
|
+
Object.defineProperty(o, Symbol.for("x.declarable"), { value: true, enumerable: true });
|
|
75
|
+
expect(isDeclarable(o)).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("a declarable symbol whose value is not true is refused", () => {
|
|
79
|
+
expect(isDeclarable(marked(Symbol.for("x.declarable"), false))).toBe(false);
|
|
80
|
+
expect(isDeclarable(marked(Symbol.for("x.declarable"), "yes"))).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("a symbol that merely contains the word is refused", () => {
|
|
84
|
+
// Suffix, not substring: `declarable.thing` is not a declarable marker.
|
|
85
|
+
expect(isDeclarable(marked(Symbol.for("x.declarable.thing")))).toBe(false);
|
|
86
|
+
expect(isDeclarable(marked(Symbol.for("notdeclarable")))).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
});
|
package/src/declarable.ts
CHANGED
|
@@ -48,15 +48,80 @@ export interface CoreOutput extends Declarable {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
*
|
|
51
|
+
* The suffix a declarable marker's symbol description ends with.
|
|
52
|
+
*
|
|
53
|
+
* chant#2444 — what makes a marker mean "entity" rather than "some other kind
|
|
54
|
+
* of chant value". `Symbol.for("chant.declarable")` has it; so does the
|
|
55
|
+
* specification host's `Symbol.for("tsad.conformance.declarable")`.
|
|
56
|
+
*/
|
|
57
|
+
const DECLARABLE_MARKER_SUFFIX = ".declarable";
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Type guard to check if a value is a Declarable.
|
|
61
|
+
*
|
|
62
|
+
* chant#2444 — accepts an entity a HOST built, not only one chant built.
|
|
63
|
+
*
|
|
64
|
+
* `F-Host-Interface` item 1 says an entity carries a non-enumerable declarable
|
|
65
|
+
* marker, and a host supplies its own: the specification's conformance host
|
|
66
|
+
* marks with `Symbol.for("tsad.conformance.declarable")`. Testing identity
|
|
67
|
+
* against chant's symbol alone refused those for not being chant's rather than
|
|
68
|
+
* for being malformed, which is what chant#2442 hit inside composite member
|
|
69
|
+
* validation and what would otherwise keep biting in the entity tallies and the
|
|
70
|
+
* revival pass-through.
|
|
71
|
+
*
|
|
72
|
+
* ## Why NOT the reference implementation's reading
|
|
73
|
+
*
|
|
74
|
+
* The reference reads L6.1 as "any own symbol or non-enumerable own property",
|
|
75
|
+
* which is right in its domain and **unsafe in chant's**. Two measurements say
|
|
76
|
+
* so, and both were taken rather than assumed:
|
|
77
|
+
*
|
|
78
|
+
* - A real chant entity's non-enumerable own NAMES are `lexicon`,
|
|
79
|
+
* `entityType`, `kind`, `props`, `attributes`, `Ref`. "Any non-enumerable
|
|
80
|
+
* own property" is therefore close to "any chant object at all".
|
|
81
|
+
* - chant has SEVEN marker symbols, not one — `chant.intrinsic`,
|
|
82
|
+
* `chant.composite`, `chant.stackOutput`, `chant.lexiconOutput`,
|
|
83
|
+
* `chant.effect-receipt`, `chant.secret-declaration`, `chant.childProject`
|
|
84
|
+
* — each a non-enumerable own symbol set to `true`. Under "any own symbol"
|
|
85
|
+
* an Intrinsic and a StackOutput both classify as Declarable, which is a
|
|
86
|
+
* worse bug than the one being fixed: they would enter the entity tallies.
|
|
87
|
+
*
|
|
88
|
+
* The reference can read any symbol because its domain has one. chant
|
|
89
|
+
* distinguishes seven kinds BY symbol identity, so it needs the marker's name.
|
|
90
|
+
* Hence the suffix: the marker must say `declarable`, whoever owns it.
|
|
91
|
+
*
|
|
92
|
+
* ## What the suffix cannot do
|
|
93
|
+
*
|
|
94
|
+
* It is a convention, not a registry. A host that marks with, say,
|
|
95
|
+
* `Symbol.for("acme.entity")` carries a perfectly good non-enumerable marker
|
|
96
|
+
* and is refused here, because nothing in this function knows that package is
|
|
97
|
+
* one the caller named. The context that would settle it —
|
|
98
|
+
* `FoldProjectOptions.lexiconPackages`, chant#2438 — lives in the fold path and
|
|
99
|
+
* does not reach a type guard called from a dozen places.
|
|
100
|
+
*
|
|
101
|
+
* That is a deliberate trade, not an oversight: a convention that refuses an
|
|
102
|
+
* unknown-but-valid marker costs a host one symbol name, while a rule loose
|
|
103
|
+
* enough to accept any marker miscounts chant's own seven kinds on every build.
|
|
104
|
+
* If a real host ever needs the other side of it, the fix is to thread the
|
|
105
|
+
* named packages through rather than to widen the test.
|
|
52
106
|
*/
|
|
53
107
|
export function isDeclarable(value: unknown): value is Declarable {
|
|
54
|
-
return
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
108
|
+
if (typeof value !== "object" || value === null) return false;
|
|
109
|
+
// chant's own, which is every entity a chant build makes.
|
|
110
|
+
if ((value as Record<symbol, unknown>)[DECLARABLE_MARKER] === true) return true;
|
|
111
|
+
return carriesForeignDeclarableMarker(value);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** A declarable marker some other implementation owns — see {@link isDeclarable}. */
|
|
115
|
+
function carriesForeignDeclarableMarker(value: object): boolean {
|
|
116
|
+
for (const marker of Object.getOwnPropertySymbols(value)) {
|
|
117
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, marker);
|
|
118
|
+
// Non-enumerable and `true`, per F-Host-Interface item 1. An enumerable
|
|
119
|
+
// symbol would travel through a spread and reach an artifact, which a
|
|
120
|
+
// marker must not.
|
|
121
|
+
if (descriptor === undefined || descriptor.enumerable || descriptor.value !== true) continue;
|
|
122
|
+
if (marker.description?.endsWith(DECLARABLE_MARKER_SUFFIX) === true) return true;
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
60
125
|
}
|
|
61
126
|
|
|
62
127
|
/**
|
|
@@ -85,6 +85,7 @@ export function generateDriverSource(options: GenerateDriverOptions): string {
|
|
|
85
85
|
`import { classifyChildError } from ${lit(CHILD_ERRORS_MODULE)};`,
|
|
86
86
|
`import { getProvenance } from ${lit(PROVENANCE_MODULE)};`,
|
|
87
87
|
`import { setBuildParams } from ${lit(PARAMS_MODULE)};`,
|
|
88
|
+
`import { writeSync } from "node:fs";`,
|
|
88
89
|
``,
|
|
89
90
|
`const BUILD_ROOT = ${lit(buildRoot)};`,
|
|
90
91
|
``,
|
|
@@ -95,7 +96,29 @@ export function generateDriverSource(options: GenerateDriverOptions): string {
|
|
|
95
96
|
// them exactly. Bound before any project import below.
|
|
96
97
|
`setBuildParams(${lit({ ...currentBuildParams })});`,
|
|
97
98
|
``,
|
|
99
|
+
// chant#2461 — which file was being imported when the loop drained.
|
|
100
|
+
//
|
|
101
|
+
// A project file whose module scope awaits something that never settles
|
|
102
|
+
// makes its `await import(...)` never complete. Nothing keeps the loop
|
|
103
|
+
// alive, so Node exits 0 having sent nothing, and `main().catch` never
|
|
104
|
+
// fires because an unsettled promise is not a rejection. The parent then
|
|
105
|
+
// knows only that the child vanished.
|
|
106
|
+
//
|
|
107
|
+
// The child DOES know, right up to the moment it exits. `process.on("exit")`
|
|
108
|
+
// runs synchronously, so it cannot `process.send` — that is asynchronous and
|
|
109
|
+
// the channel will never be serviced — but a synchronous `writeSync` to fd 2
|
|
110
|
+
// lands, and the parent already captures and forwards stderr.
|
|
111
|
+
`let importing;`,
|
|
112
|
+
`let reported = false;`,
|
|
113
|
+
`process.on("exit", () => {`,
|
|
114
|
+
` if (reported || importing === undefined) return;`,
|
|
115
|
+
` writeSync(2, "chant: this file's module scope never finished evaluating, so the build could not " +`,
|
|
116
|
+
` "report anything: " + importing + "\\n A top-level \`await\` that never settles does this. " +`,
|
|
117
|
+
` "Nothing is thrown, so nothing else can name it (chant#2461).\\n");`,
|
|
118
|
+
`});`,
|
|
119
|
+
``,
|
|
98
120
|
`function send(payload) {`,
|
|
121
|
+
` reported = true;`,
|
|
99
122
|
` if (typeof process.send === "function") process.send(payload);`,
|
|
100
123
|
` else console.log(JSON.stringify(payload));`,
|
|
101
124
|
`}`,
|
|
@@ -115,12 +138,14 @@ export function generateDriverSource(options: GenerateDriverOptions): string {
|
|
|
115
138
|
|
|
116
139
|
for (const file of files) {
|
|
117
140
|
lines.push(
|
|
141
|
+
` importing = ${lit(file)};`,
|
|
118
142
|
` try {`,
|
|
119
143
|
` const mod = await import(${lit(file)});`,
|
|
120
144
|
` modules.push({ file: ${lit(file)}, exports: mod });`,
|
|
121
145
|
` } catch (err) {`,
|
|
122
146
|
` errors.push(classifyChildError(${lit(file)}, err).toJSON());`,
|
|
123
147
|
` }`,
|
|
148
|
+
` importing = undefined;`,
|
|
124
149
|
);
|
|
125
150
|
}
|
|
126
151
|
|
|
@@ -108,7 +108,15 @@ describe("why a sandboxed child ends without a result (chant#2461)", () => {
|
|
|
108
108
|
|
|
109
109
|
const message = result.errors.map((e) => e.message).join("\n");
|
|
110
110
|
expect(message).toContain("child exited before reporting results (code 0, signal null)");
|
|
111
|
-
|
|
111
|
+
|
|
112
|
+
// chant#2461's user-facing half: the child names the file it was
|
|
113
|
+
// importing when its loop drained. It cannot `process.send` from an exit
|
|
114
|
+
// handler — that is asynchronous and the channel will never be serviced —
|
|
115
|
+
// so it writes synchronously to stderr, which the parent captures and
|
|
116
|
+
// forwards. Without this the build dies silently and nothing anywhere
|
|
117
|
+
// says which file.
|
|
118
|
+
expect(message).toContain("never finished evaluating");
|
|
119
|
+
expect(message).toContain("hangs.ts");
|
|
112
120
|
expect(message).toContain("top-level");
|
|
113
121
|
} finally {
|
|
114
122
|
rmSync(root, { recursive: true, force: true });
|
package/src/fold/subset.ts
CHANGED
|
@@ -210,13 +210,25 @@ import { intrinsicCallFolds, intrinsicCallFoldsEagerly, type IntrinsicDef } from
|
|
|
210
210
|
* binding whatever an invoked factory returns is what chant never checked
|
|
211
211
|
* (`L8.19`), and a declarator reaching a call through a const alias is what
|
|
212
212
|
* chant resolves while refusing a call nested elsewhere (`L2.18`).
|
|
213
|
+
* - **1.7** put `F-Eval-CallLocal` before the two registered call shapes, so a
|
|
214
|
+
* call through a name the project bound is the project function's call
|
|
215
|
+
* whatever the registry says. That is what chant's whole-build fold already
|
|
216
|
+
* did, and the reason the specification moved rather than chant: probes with
|
|
217
|
+
* chant's own `output` showed the binding-first order living in
|
|
218
|
+
* `resolveCallExpression`, while `L2.11` described the expression
|
|
219
|
+
* classifier. The two were never required to share an order.
|
|
220
|
+
* - **1.8** added `ι = executing`, and chant carries it as of `0.72.3`
|
|
221
|
+
* (#2455) — `FoldProjectOptions.executing`, mutually exclusive with
|
|
222
|
+
* `sandbox`. `open` stays the default and stays strict (#2453).
|
|
213
223
|
*
|
|
214
224
|
* `scripts/check-docs-citations.ts` refuses to run when this constant and the
|
|
215
225
|
* pinned specification disagree, so `.github/workflows/docs-check.yml` pins the
|
|
216
|
-
* `spec-1.
|
|
217
|
-
* is what stops the docs being gated against a rule set nobody writes against
|
|
226
|
+
* `spec-1.8` commit and the two move together. That coupling is deliberate: it
|
|
227
|
+
* is what stops the docs being gated against a rule set nobody writes against
|
|
228
|
+
* — which is exactly what happened while this sat at `1.0` through six
|
|
229
|
+
* versions, passing because both sides were equally stale.
|
|
218
230
|
*/
|
|
219
|
-
export const SPEC_VERSION = "1.
|
|
231
|
+
export const SPEC_VERSION = "1.8";
|
|
220
232
|
|
|
221
233
|
/** The two EVL rule ids a shape violation can be attributed to. */
|
|
222
234
|
export type SubsetRuleId = "EVL001" | "EVL003";
|