@intentius/chant 0.124.14 → 0.124.16
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.d.ts +6 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +1 -0
- package/dist/change-set.d.ts +24 -2
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +50 -1
- package/dist/ci-jobs.d.ts +227 -0
- package/dist/ci-jobs.d.ts.map +1 -0
- package/dist/ci-jobs.js +448 -0
- package/dist/cli/flush.d.ts +16 -0
- package/dist/cli/flush.d.ts.map +1 -0
- package/dist/cli/flush.js +42 -0
- package/dist/cli/handlers/approve.d.ts +98 -0
- package/dist/cli/handlers/approve.d.ts.map +1 -0
- package/dist/cli/handlers/approve.js +495 -0
- package/dist/cli/handlers/operator.d.ts +1 -95
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/operator.js +4 -488
- package/dist/cli/handlers/override-approve.js +1 -1
- package/dist/cli/main.d.ts +4 -15
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +4 -1062
- package/dist/cli/parse-args.d.ts +6 -0
- package/dist/cli/parse-args.d.ts.map +1 -0
- package/dist/cli/parse-args.js +1028 -0
- package/dist/components/fan-out-record.d.ts +1 -1
- package/dist/components/fan-out-record.d.ts.map +1 -1
- package/dist/components/fan-out-record.js +3 -7
- package/dist/components/fan-out-run.d.ts +5 -3
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +7 -5
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +3 -2
- package/dist/composite.d.ts +10 -2
- package/dist/composite.d.ts.map +1 -1
- package/dist/composite.js +8 -0
- package/dist/discovery/collect.d.ts +2 -1
- package/dist/discovery/collect.d.ts.map +1 -1
- package/dist/discovery/collect.js +16 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/fold-import.js +2 -107
- package/dist/discovery/host-composite.d.ts +35 -0
- package/dist/discovery/host-composite.d.ts.map +1 -0
- package/dist/discovery/host-composite.js +93 -0
- package/dist/discovery/index.d.ts +9 -0
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/index.js +1 -1
- package/dist/gate-decision.d.ts +20 -9
- package/dist/gate-decision.d.ts.map +1 -1
- package/dist/gate-decision.js +72 -19
- package/dist/gated-waves.d.ts +25 -6
- package/dist/gated-waves.d.ts.map +1 -1
- package/dist/gated-waves.js +21 -6
- package/dist/lifecycle/gate-audit.d.ts +69 -2
- package/dist/lifecycle/gate-audit.d.ts.map +1 -1
- package/dist/lifecycle/gate-audit.js +76 -2
- package/dist/lifecycle/gate-ledger.d.ts +24 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.js +9 -1
- package/dist/op/activities/activity-contracts.d.ts +5 -5
- package/dist/op/activities/activity-contracts.js +1 -1
- package/dist/op/activities/change-set.d.ts +8 -3
- package/dist/op/activities/change-set.d.ts.map +1 -1
- package/dist/op/activities/change-set.js +5 -2
- package/dist/op/activities/reconcile.d.ts +28 -16
- package/dist/op/activities/reconcile.d.ts.map +1 -1
- package/dist/op/activities/reconcile.js +34 -20
- package/dist/op/builders.d.ts +1 -1
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/builders.js +1 -1
- package/dist/op/op-waves-run.d.ts +2 -2
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +8 -9
- package/dist/op/op-waves.d.ts.map +1 -1
- package/dist/op/op-waves.js +4 -8
- package/dist/package-version.d.ts +2 -0
- package/dist/package-version.d.ts.map +1 -0
- package/dist/package-version.js +10 -0
- package/dist/wave-engine.d.ts +62 -0
- package/dist/wave-engine.d.ts.map +1 -0
- package/dist/wave-engine.js +59 -0
- package/dist/wave-layout.d.ts +29 -0
- package/dist/wave-layout.d.ts.map +1 -0
- package/dist/wave-layout.js +41 -0
- package/dist/wave-runtime.d.ts +29 -0
- package/dist/wave-runtime.d.ts.map +1 -0
- package/dist/wave-runtime.js +103 -0
- package/dist/wave-runtime.mjs +331 -0
- package/dist/workspace/__fixtures__/contract-repo.d.ts +7 -0
- package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/contract-repo.js +17 -1
- package/dist/workspace/__fixtures__/sessions.d.ts +1 -1
- package/dist/workspace/__fixtures__/sessions.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/sessions.js +3 -3
- package/dist/workspace/ajv2020.d.ts +5 -0
- package/dist/workspace/ajv2020.d.ts.map +1 -0
- package/dist/workspace/ajv2020.js +14 -0
- package/dist/workspace/change-set.schema.json +5 -3
- package/dist/workspace/ci-green-workflow.d.ts +3 -1
- package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
- package/dist/workspace/ci-green-workflow.js +5 -2
- package/dist/workspace/class-data.d.ts +84 -0
- package/dist/workspace/class-data.d.ts.map +1 -0
- package/dist/workspace/class-data.js +238 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +4 -8
- package/dist/workspace/kinds.d.ts +3 -7
- package/dist/workspace/kinds.d.ts.map +1 -1
- package/dist/workspace/kinds.js +8 -45
- package/dist/workspace/points.d.ts.map +1 -1
- package/dist/workspace/points.js +2 -5
- package/dist/workspace/principal-classes.d.ts +4 -11
- package/dist/workspace/principal-classes.d.ts.map +1 -1
- package/dist/workspace/principal-classes.js +13 -99
- package/package.json +5 -3
- package/src/build.ts +8 -0
- package/src/change-set.test.ts +48 -0
- package/src/change-set.ts +52 -2
- package/src/ci-jobs.test.ts +49 -0
- package/src/ci-jobs.ts +515 -0
- package/src/cli/flush.ts +44 -0
- package/src/cli/handlers/approve.ts +618 -0
- package/src/cli/handlers/operator.ts +10 -608
- package/src/cli/handlers/override-approve.test.ts +1 -1
- package/src/cli/handlers/override-approve.ts +1 -1
- package/src/cli/main.ts +5 -816
- package/src/cli/parse-args.ts +781 -0
- package/src/components/fan-out-record.ts +3 -7
- package/src/components/fan-out-run.ts +7 -5
- package/src/components/pr-pipeline.ts +3 -2
- package/src/composite.ts +15 -3
- package/src/discovery/collect.ts +15 -1
- package/src/discovery/fold-build-parity.test.ts +71 -0
- package/src/discovery/fold-import.ts +2 -113
- package/src/discovery/fold-project-provenance.test.ts +21 -4
- package/src/discovery/host-composite.ts +101 -0
- package/src/discovery/index.ts +11 -1
- package/src/gate-decision-bundle.test.ts +6 -4
- package/src/gate-decision-identity.test.ts +59 -6
- package/src/gate-decision.ts +81 -21
- package/src/gated-waves.ts +28 -6
- package/src/lifecycle/gate-audit.test.ts +55 -1
- package/src/lifecycle/gate-audit.ts +116 -3
- package/src/lifecycle/gate-ledger.test.ts +20 -1
- package/src/lifecycle/gate-ledger.ts +33 -1
- package/src/op/activities/activity-contracts.ts +1 -1
- package/src/op/activities/change-set.ts +11 -3
- package/src/op/activities/lexicon-upgrade.test.ts +2 -0
- package/src/op/activities/reconcile.test.ts +62 -2
- package/src/op/activities/reconcile.ts +46 -22
- package/src/op/builders.ts +1 -1
- package/src/op/op-waves-run.ts +9 -9
- package/src/op/op-waves.test.ts +2 -0
- package/src/op/op-waves.ts +4 -8
- package/src/op/work-lease-run.test.ts +12 -4
- package/src/package-version.ts +11 -0
- package/src/wave-engine.test.ts +107 -0
- package/src/wave-engine.ts +98 -0
- package/src/wave-layout.ts +44 -0
- package/src/wave-runtime-bundle.test.ts +70 -0
- package/src/wave-runtime.e2e.test.ts +177 -0
- package/src/wave-runtime.ts +104 -0
- package/src/workspace/__fixtures__/contract-repo.ts +17 -1
- package/src/workspace/__fixtures__/sessions.ts +3 -3
- package/src/workspace/ajv2020.ts +17 -0
- package/src/workspace/change-set.schema.json +5 -3
- package/src/workspace/ci-green-workflow.test.ts +4 -4
- package/src/workspace/ci-green-workflow.ts +5 -2
- package/src/workspace/class-data.test.ts +64 -0
- package/src/workspace/class-data.ts +236 -0
- package/src/workspace/declaration.ts +4 -10
- package/src/workspace/kinds.ts +8 -40
- package/src/workspace/points.ts +2 -5
- package/src/workspace/principal-classes.ts +13 -99
- package/src/workspace/records-formats.test.ts +7 -2
- package/src/workspace/records-sessions-write.test.ts +3 -3
- package/src/workspace/status-contract.e2e.test.ts +37 -0
- package/src/workspace/status-contract.test.ts +23 -21
|
@@ -27,9 +27,8 @@
|
|
|
27
27
|
* per-wave CI job hands to the next.
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import { readWaveRecords, type WaveRecord } from "../gated-waves";
|
|
30
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
31
|
+
import { readWaveRecords, writeWaveRecordFile, type WaveRecord } from "../wave-engine";
|
|
33
32
|
import type { ChangeSetDocument } from "../change-set";
|
|
34
33
|
import type { PrMember } from "../pr-loop";
|
|
35
34
|
|
|
@@ -126,8 +125,5 @@ export function readFanOutAttempt(path: string): FanOutAttempt | undefined {
|
|
|
126
125
|
* write leaves the previous record whole rather than half a JSON document.
|
|
127
126
|
*/
|
|
128
127
|
export function writeFanOutAttempt(path: string, attempt: FanOutAttempt): void {
|
|
129
|
-
|
|
130
|
-
const temporary = `${path}.${process.pid}.tmp`;
|
|
131
|
-
writeFileSync(temporary, JSON.stringify(attempt, null, 2) + "\n");
|
|
132
|
-
renameSync(temporary, path);
|
|
128
|
+
writeWaveRecordFile(path, attempt);
|
|
133
129
|
}
|
|
@@ -31,8 +31,10 @@
|
|
|
31
31
|
* ## Or one approval per wave
|
|
32
32
|
*
|
|
33
33
|
* `waveGate` (#3049) is the other mode. Each wave is planned once the waves
|
|
34
|
-
* before it applied, and its gate binds that wave's set digest
|
|
35
|
-
*
|
|
34
|
+
* before it applied, and its gate binds that wave's set digest. Gate names,
|
|
35
|
+
* the digest and the gate decision come from the wave engine
|
|
36
|
+
* (`../wave-engine.ts`, #3766), which `chant run wave` uses too. See
|
|
37
|
+
* {@link runGatedWaves}.
|
|
36
38
|
*/
|
|
37
39
|
|
|
38
40
|
import {
|
|
@@ -50,7 +52,7 @@ import type { CapabilityRegistry } from "./capability";
|
|
|
50
52
|
import type { PendingGateRecord } from "../lifecycle/gate-ledger";
|
|
51
53
|
import type { DeployContext } from "./capability";
|
|
52
54
|
import { planWaveComponent, type WaveComponentPlan } from "./wave-plan";
|
|
53
|
-
import { waveGateName, waveSetDigest, type WaveMember, type WaveRecord } from "../
|
|
55
|
+
import { decideWaveGate, waveGateName, waveSetDigest, type WaveMember, type WaveRecord } from "../wave-engine";
|
|
54
56
|
|
|
55
57
|
/**
|
|
56
58
|
* One gate per wave (#3049), each bound to its wave's set digest. The other
|
|
@@ -452,10 +454,10 @@ async function runGatedWaves(
|
|
|
452
454
|
continue;
|
|
453
455
|
}
|
|
454
456
|
|
|
455
|
-
const check = await
|
|
457
|
+
const check = await decideWaveGate(gates.port, {
|
|
456
458
|
op: waveGate.op,
|
|
457
459
|
gate: gateName,
|
|
458
|
-
|
|
460
|
+
digest,
|
|
459
461
|
description: waveGate.description ?? `wave ${waveNum} of ${plan.waves.length}: ${toRun.join(", ")}`,
|
|
460
462
|
...(waveGate.timeout ? { timeout: waveGate.timeout } : {}),
|
|
461
463
|
...(options.now ? { now: options.now } : {}),
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
|
|
39
39
|
import type { ForgeKind } from "../pr-forge";
|
|
40
40
|
import { PR_APPLY_GATE } from "../pr-loop";
|
|
41
|
+
import { CI_IMAGE, GITHUB_JOB_TOKEN } from "../ci-jobs";
|
|
41
42
|
|
|
42
43
|
/** Where both jobs write their reports, kept as a CI artifact. */
|
|
43
44
|
export const PR_LOOP_REPORT_DIR = ".chant/pr";
|
|
@@ -55,7 +56,7 @@ export const PR_APPLY_RECORD = ".chant/pr-resume/pr-apply.json";
|
|
|
55
56
|
* `PR_LOOP_SETUP` adds OpenTofu on top of it. A caller who sets `image`
|
|
56
57
|
* brings both and gets no setup lines.
|
|
57
58
|
*/
|
|
58
|
-
export const PR_LOOP_IMAGE =
|
|
59
|
+
export const PR_LOOP_IMAGE = CI_IMAGE;
|
|
59
60
|
|
|
60
61
|
/** Installs OpenTofu into `PR_LOOP_IMAGE`. */
|
|
61
62
|
export const PR_LOOP_SETUP = [
|
|
@@ -113,7 +114,7 @@ export function prLoopJobs(forge: ForgeKind, env: string, options: PrLoopPipelin
|
|
|
113
114
|
...(member ? ["--member", member] : []),
|
|
114
115
|
];
|
|
115
116
|
const github = forge !== "gitlab";
|
|
116
|
-
const token: Record<string, string> = github ? { GITHUB_TOKEN:
|
|
117
|
+
const token: Record<string, string> = github ? { GITHUB_TOKEN: GITHUB_JOB_TOKEN } : {};
|
|
117
118
|
return [
|
|
118
119
|
{
|
|
119
120
|
jobName: "plan",
|
package/src/composite.ts
CHANGED
|
@@ -115,11 +115,23 @@ export class CompositeRegistry {
|
|
|
115
115
|
* });
|
|
116
116
|
* export const storage = SecureStorage({ name: "data" });
|
|
117
117
|
* ```
|
|
118
|
+
*
|
|
119
|
+
* `D` (chant#3787) lets a lexicon give the definition a more precise call
|
|
120
|
+
* signature than `(props: P) => M`, such as a generic one whose members' types
|
|
121
|
+
* depend on the literal props it is called with. It is a type only: the value
|
|
122
|
+
* is the same definition, and `D` must still be a `CompositeDefinition<P, M>`,
|
|
123
|
+
* so an intersection with the precise signature first is the usual form. It
|
|
124
|
+
* is passed as a type argument rather than by casting the result, because the
|
|
125
|
+
* fold recognizes a composite by its `export const X = Composite(...)` shape.
|
|
118
126
|
*/
|
|
119
|
-
export function Composite<
|
|
127
|
+
export function Composite<
|
|
128
|
+
P,
|
|
129
|
+
M extends CompositeFactoryMembers = CompositeMembers,
|
|
130
|
+
D extends CompositeDefinition<P, M> = CompositeDefinition<P, M>,
|
|
131
|
+
>(
|
|
120
132
|
factory: (props: P) => M,
|
|
121
133
|
name?: string,
|
|
122
|
-
):
|
|
134
|
+
): D {
|
|
123
135
|
const id = Symbol();
|
|
124
136
|
const compositeName = name ?? "anonymous";
|
|
125
137
|
|
|
@@ -163,7 +175,7 @@ export function Composite<P, M extends CompositeFactoryMembers = CompositeMember
|
|
|
163
175
|
|
|
164
176
|
CompositeRegistry.register(definition as CompositeDefinition<unknown>);
|
|
165
177
|
|
|
166
|
-
return definition;
|
|
178
|
+
return definition as D;
|
|
167
179
|
}
|
|
168
180
|
|
|
169
181
|
/** Non-enumerable, symbol-keyed: invisible to serializers, spreads and comparisons, like provenance. */
|
package/src/discovery/collect.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { isCompositeInstance, expandComposite, compositeOfMember } from "../comp
|
|
|
4
4
|
import { isLexiconOutput } from "../lexicon-output";
|
|
5
5
|
import { DiscoveryError } from "../errors";
|
|
6
6
|
import { setProvenance, type EntityProvenance } from "../provenance";
|
|
7
|
+
import { expandHostComposite, hostCompositeName } from "./host-composite";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* The entity key for an export. `export default` is per-module — the `Op` pattern
|
|
@@ -119,6 +120,18 @@ function enumerateEntries(
|
|
|
119
120
|
provenance: { sourceFile: file, compositeInstance: name },
|
|
120
121
|
});
|
|
121
122
|
}
|
|
123
|
+
} else if (hostCompositeName(value) !== undefined) {
|
|
124
|
+
// chant#3612 — a host composite instance the fold resolved, expanded
|
|
125
|
+
// here rather than by one caller, so `build()` and `foldProject`
|
|
126
|
+
// report the same entities for it. See ./host-composite.ts.
|
|
127
|
+
for (const [expandedName, entity] of expandHostComposite(name, value as Record<string, unknown>)) {
|
|
128
|
+
entries.push({
|
|
129
|
+
bareKey: expandedName,
|
|
130
|
+
value: entity,
|
|
131
|
+
file,
|
|
132
|
+
provenance: { sourceFile: file, compositeInstance: name },
|
|
133
|
+
});
|
|
134
|
+
}
|
|
122
135
|
} else if (isLexiconOutput(value)) {
|
|
123
136
|
// LexiconOutput is not a Declarable but build() expects to find them in
|
|
124
137
|
// the entities map so it can collect and pass them to serializers.
|
|
@@ -133,7 +146,8 @@ function enumerateEntries(
|
|
|
133
146
|
/**
|
|
134
147
|
* Collects all declarable entities from imported modules.
|
|
135
148
|
* CompositeInstance exports are expanded into individual entities
|
|
136
|
-
* with `{exportName}_{memberName}` naming
|
|
149
|
+
* with `{exportName}_{memberName}` naming, and so are host composite
|
|
150
|
+
* instances the fold resolved (chant#3612, ./host-composite.ts).
|
|
137
151
|
* LexiconOutput exports are also collected so that build() can
|
|
138
152
|
* extract them and pass them to the serializer.
|
|
139
153
|
*
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chant#3612 — `build()` and `foldProject` report the same entities, under the
|
|
3
|
+
* same names, with the same provenance, for projects that use composites.
|
|
4
|
+
*
|
|
5
|
+
* The two share `collectEntities` and `foldProvenanceOfEntities`; what could
|
|
6
|
+
* still differ is what each hands them. These fixtures are real examples, one
|
|
7
|
+
* per lexicon whose composites a project calls, built the way `chant build`
|
|
8
|
+
* builds them (fold on, the plugins' intrinsics, the lexicon names).
|
|
9
|
+
*/
|
|
10
|
+
import { describe, test, expect, beforeAll } from "vitest";
|
|
11
|
+
import { join, resolve, dirname } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { build } from "../build";
|
|
14
|
+
import { foldProject, type FoldProvenance } from "../index";
|
|
15
|
+
import { findInfraFiles } from "./files";
|
|
16
|
+
import { loadPlugins, resolveProjectLexicons } from "../cli/plugins";
|
|
17
|
+
import type { LexiconPlugin } from "../lexicon";
|
|
18
|
+
|
|
19
|
+
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../../..");
|
|
20
|
+
|
|
21
|
+
const FIXTURES = [
|
|
22
|
+
"lexicons/aws/examples/fargate-alb",
|
|
23
|
+
"lexicons/aws/examples/lambda-function",
|
|
24
|
+
"lexicons/aws/examples/multi-service-alb",
|
|
25
|
+
"lexicons/k8s/examples/batch-workers",
|
|
26
|
+
"lexicons/k8s/examples/web-platform",
|
|
27
|
+
"lexicons/sql/examples/postgres-composites",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
describe("build() and foldProject agree on composite-using projects (chant#3612)", () => {
|
|
31
|
+
// Loading a lexicon (aws's generated barrel above all) is the slow part, and
|
|
32
|
+
// it is not what the tests measure, so it happens once, up front.
|
|
33
|
+
const pluginsOf = new Map<string, LexiconPlugin[]>();
|
|
34
|
+
beforeAll(async () => {
|
|
35
|
+
for (const fixture of FIXTURES) {
|
|
36
|
+
const project = join(repoRoot, fixture);
|
|
37
|
+
pluginsOf.set(fixture, await loadPlugins(await resolveProjectLexicons(project)));
|
|
38
|
+
}
|
|
39
|
+
}, 60_000);
|
|
40
|
+
|
|
41
|
+
test.each(FIXTURES)("%s", async (fixture) => {
|
|
42
|
+
const src = join(repoRoot, fixture, "src");
|
|
43
|
+
const plugins = pluginsOf.get(fixture)!;
|
|
44
|
+
const intrinsics = plugins.flatMap((p) => p.intrinsics?.() ?? []);
|
|
45
|
+
const lexicons = plugins.map((p) => p.name);
|
|
46
|
+
|
|
47
|
+
const built = await build(src, plugins.map((p) => p.serializer), undefined, { fold: true, intrinsics, lexicons });
|
|
48
|
+
expect(built.errors).toEqual([]);
|
|
49
|
+
|
|
50
|
+
const files = await findInfraFiles(src);
|
|
51
|
+
const verdicts = await foldProject(files, intrinsics, { lexicons });
|
|
52
|
+
// Same verdict per file. A file that runs (a `chant.config.ts` in `src`)
|
|
53
|
+
// has no fold provenance in foldProject, so it is left out of both sides.
|
|
54
|
+
const mode = new Map(built.foldDecisions.map((d) => [d.file, d.mode]));
|
|
55
|
+
const folded: FoldProvenance = {};
|
|
56
|
+
for (const [file, verdict] of verdicts) {
|
|
57
|
+
expect({ file, verdict: verdict.verdict }).toEqual({ file, verdict: mode.get(file) });
|
|
58
|
+
Object.assign(folded, verdict.foldProvenance);
|
|
59
|
+
}
|
|
60
|
+
const foldedFiles = new Set(files.filter((file) => verdicts.get(file)!.verdict === "fold"));
|
|
61
|
+
expect(foldedFiles.size).toBeGreaterThan(0);
|
|
62
|
+
const fromBuild = Object.fromEntries(
|
|
63
|
+
Object.entries(built.foldProvenance).filter(([, record]) => foldedFiles.has(record.sourceFile ?? "")),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// The fixture has to exercise a composite, or the comparison proves nothing.
|
|
67
|
+
expect(Object.values(fromBuild).some((record) => record.composite !== undefined)).toBe(true);
|
|
68
|
+
expect(Object.keys(folded).sort()).toEqual(Object.keys(fromBuild).sort());
|
|
69
|
+
expect(folded).toEqual(fromBuild);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
import { getInterpolationFields, getProvenance, setPathProvenance, setProvenance } from "../provenance";
|
|
61
61
|
import { foldProvenanceOfEntities, type FoldProvenance } from "../fold-provenance";
|
|
62
62
|
import { collectEntities } from "./collect";
|
|
63
|
+
import { hostCompositeName, markHostCompositeInstance } from "./host-composite";
|
|
63
64
|
import { intrinsicCallFoldsEagerly, intrinsicTagFolds, type IntrinsicDef } from "../lexicon";
|
|
64
65
|
import type { BuildParamValue } from "../build-params";
|
|
65
66
|
|
|
@@ -3056,53 +3057,6 @@ async function interpretCompositeFactory(
|
|
|
3056
3057
|
}
|
|
3057
3058
|
}
|
|
3058
3059
|
|
|
3059
|
-
/**
|
|
3060
|
-
* chant#3598 — a host composite instance's own marker. Non-enumerable and
|
|
3061
|
-
* symbol-keyed, like provenance, so nothing that serializes or compares the
|
|
3062
|
-
* exports sees it. It carries the composite's name, which the members object
|
|
3063
|
-
* otherwise has no record of once it is returned.
|
|
3064
|
-
*/
|
|
3065
|
-
const HOST_COMPOSITE_INSTANCE = Symbol.for("chant.fold.hostCompositeInstance");
|
|
3066
|
-
|
|
3067
|
-
/**
|
|
3068
|
-
* Mark `members` as the instance a host's composite produced, and return it
|
|
3069
|
-
* unchanged.
|
|
3070
|
-
*
|
|
3071
|
-
* chant#3608 — every declarable member, and every member of a nested host
|
|
3072
|
-
* instance, is also stamped with the composite, the way `expandComposite`
|
|
3073
|
-
* stamps a chant instance's members. A destructured member is never expanded,
|
|
3074
|
-
* so this is the only record it has that a composite wrote it, and without
|
|
3075
|
-
* it a path the fold did not attribute reads as authored and so `direct`.
|
|
3076
|
-
* `setProvenance` keeps the first writer, and a nested instance was marked
|
|
3077
|
-
* when its own call returned, so a member keeps its innermost composite.
|
|
3078
|
-
*/
|
|
3079
|
-
function markHostCompositeInstance(members: Record<string, unknown>, compositeName: string): Record<string, unknown> {
|
|
3080
|
-
if (Object.isExtensible(members) && !Object.prototype.hasOwnProperty.call(members, HOST_COMPOSITE_INSTANCE)) {
|
|
3081
|
-
Object.defineProperty(members, HOST_COMPOSITE_INSTANCE, {
|
|
3082
|
-
value: compositeName,
|
|
3083
|
-
enumerable: false,
|
|
3084
|
-
configurable: true,
|
|
3085
|
-
});
|
|
3086
|
-
}
|
|
3087
|
-
stampHostCompositeMembers(members, compositeName);
|
|
3088
|
-
return members;
|
|
3089
|
-
}
|
|
3090
|
-
|
|
3091
|
-
/** Stamp `composite` on each declarable member of a host instance, descending into nested host instances. */
|
|
3092
|
-
function stampHostCompositeMembers(members: Record<string, unknown>, compositeName: string): void {
|
|
3093
|
-
for (const member of Object.values(members)) {
|
|
3094
|
-
if (isDeclarable(member)) setProvenance(member, { composite: compositeName });
|
|
3095
|
-
else if (hostCompositeName(member) !== undefined) stampHostCompositeMembers(member as Record<string, unknown>, compositeName);
|
|
3096
|
-
}
|
|
3097
|
-
}
|
|
3098
|
-
|
|
3099
|
-
/** The composite name a host composite instance was marked with, or `undefined` for any other value. */
|
|
3100
|
-
function hostCompositeName(value: unknown): string | undefined {
|
|
3101
|
-
if (!isIndexableObject(value)) return undefined;
|
|
3102
|
-
const name = (value as Record<symbol, unknown>)[HOST_COMPOSITE_INSTANCE];
|
|
3103
|
-
return typeof name === "string" ? name : undefined;
|
|
3104
|
-
}
|
|
3105
|
-
|
|
3106
3060
|
/** Evaluate an admissible factory body's statements in order, binding each `const`, and return the `return` expression's value. */
|
|
3107
3061
|
async function interpretFactoryBody(
|
|
3108
3062
|
fn: ts.ArrowFunction | ts.FunctionExpression,
|
|
@@ -5081,7 +5035,7 @@ export async function foldProject(
|
|
|
5081
5035
|
const attempt = attempts.get(file);
|
|
5082
5036
|
return {
|
|
5083
5037
|
file,
|
|
5084
|
-
exports: attempt?.ok === true ?
|
|
5038
|
+
exports: attempt?.ok === true ? Object.fromEntries(attempt.exportedValues) : {},
|
|
5085
5039
|
};
|
|
5086
5040
|
}),
|
|
5087
5041
|
);
|
|
@@ -5153,71 +5107,6 @@ function foldProvenanceByFile(
|
|
|
5153
5107
|
return out;
|
|
5154
5108
|
}
|
|
5155
5109
|
|
|
5156
|
-
/**
|
|
5157
|
-
* chant#3598 — a file's exports with every host composite instance replaced by
|
|
5158
|
-
* its members, for {@link foldProvenanceByFile} only.
|
|
5159
|
-
*
|
|
5160
|
-
* A host's registration form (chant#2442) returns the members object the
|
|
5161
|
-
* factory built, not a chant `CompositeInstance`, because wrapping it would run
|
|
5162
|
-
* chant's member validation against the host's own entities. `collectEntities`
|
|
5163
|
-
* expands only chant's instances, so an instance exported whole
|
|
5164
|
-
* (`export const store = Store({...})`) contributed no entity and therefore no
|
|
5165
|
-
* record, while a destructured member, a plain declarable export, did.
|
|
5166
|
-
*
|
|
5167
|
-
* The members are named and stamped the way `expandComposite` names and stamps
|
|
5168
|
-
* a chant instance: `${exportName}${Member}`, with a whole-entity `composite`
|
|
5169
|
-
* origin under the paths the fold already attributed. That origin is written
|
|
5170
|
-
* before `collectEntities` adds its `authored` one, and the first writer wins,
|
|
5171
|
-
* so a path the fold did not attribute is `unknown` rather than `direct`.
|
|
5172
|
-
*
|
|
5173
|
-
* Only a top-level export is expanded. A host instance nested in another
|
|
5174
|
-
* composite's body or placed in an array does not fold today, so its file is
|
|
5175
|
-
* `run` and has no record to give.
|
|
5176
|
-
*
|
|
5177
|
-
* The exports a caller receives in the verdict are the originals.
|
|
5178
|
-
*/
|
|
5179
|
-
function expandHostCompositeExports(exports: Record<string, unknown>): Record<string, unknown> {
|
|
5180
|
-
const out: Record<string, unknown> = {};
|
|
5181
|
-
for (const [name, value] of Object.entries(exports)) {
|
|
5182
|
-
if (hostCompositeName(value) === undefined) {
|
|
5183
|
-
out[name] = value;
|
|
5184
|
-
continue;
|
|
5185
|
-
}
|
|
5186
|
-
expandHostCompositeInstance(name, value as Record<string, unknown>, name, out);
|
|
5187
|
-
}
|
|
5188
|
-
return out;
|
|
5189
|
-
}
|
|
5190
|
-
|
|
5191
|
-
/**
|
|
5192
|
-
* chant#3608 — one host instance's members into `out`, a nested host instance
|
|
5193
|
-
* expanded in place under its member's name (`siteMainBucket`), as
|
|
5194
|
-
* `expandComposite` expands a nested chant instance. Every member belongs to
|
|
5195
|
-
* the top-level `instance`; its composite is the innermost one, which
|
|
5196
|
-
* {@link markHostCompositeInstance} already stamped, so the whole-entity
|
|
5197
|
-
* origin names that composite too.
|
|
5198
|
-
*/
|
|
5199
|
-
function expandHostCompositeInstance(
|
|
5200
|
-
prefix: string,
|
|
5201
|
-
members: Record<string, unknown>,
|
|
5202
|
-
instance: string,
|
|
5203
|
-
out: Record<string, unknown>,
|
|
5204
|
-
): void {
|
|
5205
|
-
const compositeName = hostCompositeName(members) as string;
|
|
5206
|
-
for (const [memberName, member] of Object.entries(members)) {
|
|
5207
|
-
if (memberName.length === 0) continue;
|
|
5208
|
-
const fullName = `${prefix}${memberName[0].toUpperCase()}${memberName.slice(1)}`;
|
|
5209
|
-
if (hostCompositeName(member) !== undefined) {
|
|
5210
|
-
expandHostCompositeInstance(fullName, member as Record<string, unknown>, instance, out);
|
|
5211
|
-
continue;
|
|
5212
|
-
}
|
|
5213
|
-
if (!isDeclarable(member)) continue;
|
|
5214
|
-
setProvenance(member, { composite: compositeName, compositeInstance: instance });
|
|
5215
|
-
const composite = getProvenance(member)?.composite ?? compositeName;
|
|
5216
|
-
setPathProvenance(member, "", { kind: "composite", composite, instance });
|
|
5217
|
-
out[fullName] = member;
|
|
5218
|
-
}
|
|
5219
|
-
}
|
|
5220
|
-
|
|
5221
5110
|
/** The deepest directory every one of `files` sits under. */
|
|
5222
5111
|
function commonDirectory(files: readonly string[]): string {
|
|
5223
5112
|
const split = files.map((file) => dirname(resolvePath(file)).split(sep));
|
|
@@ -394,11 +394,15 @@ describe("foldProject provenance for host composites (chant#3608)", () => {
|
|
|
394
394
|
expect(provenance.bucket.composite).toBe("Archive");
|
|
395
395
|
expect(provenance.bucket.fields).toEqual({ name: unknown, versioned: unknown });
|
|
396
396
|
|
|
397
|
-
// build()
|
|
398
|
-
|
|
399
|
-
|
|
397
|
+
// chant#3612 — build() expands the whole instance too, and agrees on every record.
|
|
398
|
+
const built = await build(join(root, "archive"), [namesSerializer], undefined, {
|
|
399
|
+
fold: true,
|
|
400
|
+
lexiconPackages: ["@tsad/shapes-3608"],
|
|
401
|
+
sandbox: true,
|
|
402
|
+
});
|
|
400
403
|
expect(built.errors).toEqual([]);
|
|
401
|
-
expect(built.
|
|
404
|
+
expect([...built.entities.keys()].sort()).toEqual(["archiveBucket", "bucket"]);
|
|
405
|
+
expect(built.foldProvenance).toEqual(provenance);
|
|
402
406
|
});
|
|
403
407
|
|
|
404
408
|
test("a whole instance's nested composite members have records, and the innermost composite wins", async () => {
|
|
@@ -458,6 +462,19 @@ describe("foldProject provenance for host composites (chant#3608)", () => {
|
|
|
458
462
|
expect(provenance.mainBucket.fields).toMatchObject({ name: innerName("main"), tier: innerTier("main") });
|
|
459
463
|
expect(provenance.fixedBucket.fields).toMatchObject({ name: innerName("fixed"), tier: innerTier("fixed") });
|
|
460
464
|
expect(provenance.side.fields.name).toMatchObject({ kind: "composite-parameter", composite: "Outer", parameters: ["name"] });
|
|
465
|
+
|
|
466
|
+
// chant#3612 — build() given the same host package reports the same
|
|
467
|
+
// entities and the same records. Before, it emitted nothing for `site`
|
|
468
|
+
// and, with the host off the allowlist, ran the file and read `side` as direct.
|
|
469
|
+
const built = await build(join(root, "site"), [namesSerializer], undefined, {
|
|
470
|
+
fold: true,
|
|
471
|
+
lexiconPackages: ["@tsad/shapes-3608"],
|
|
472
|
+
sandbox: true,
|
|
473
|
+
});
|
|
474
|
+
expect(built.errors).toEqual([]);
|
|
475
|
+
expect(built.foldDecisions.map((d) => d.mode)).toEqual(["fold"]);
|
|
476
|
+
expect([...built.entities.keys()].sort()).toEqual(Object.keys(provenance).sort());
|
|
477
|
+
expect(built.foldProvenance).toEqual(provenance);
|
|
461
478
|
});
|
|
462
479
|
});
|
|
463
480
|
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { isDeclarable, type Declarable } from "../declarable";
|
|
2
|
+
import { getProvenance, setPathProvenance, setProvenance } from "../provenance";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* chant#3598 — a host composite instance's own marker. Non-enumerable and
|
|
6
|
+
* symbol-keyed, like provenance, so nothing that serializes or compares the
|
|
7
|
+
* exports sees it. It carries the composite's name, which the members object
|
|
8
|
+
* otherwise has no record of once it is returned.
|
|
9
|
+
*
|
|
10
|
+
* Only the fold path sets it: a host's registration form (chant#2442) returns
|
|
11
|
+
* the members object the factory built, not a chant `CompositeInstance`, and
|
|
12
|
+
* chant knows the object came from a composite only because the fold resolved
|
|
13
|
+
* the call. A file that runs instead has no such knowledge.
|
|
14
|
+
*/
|
|
15
|
+
const HOST_COMPOSITE_INSTANCE = Symbol.for("chant.fold.hostCompositeInstance");
|
|
16
|
+
|
|
17
|
+
function isPlainIndexable(value: unknown): value is Record<string, unknown> {
|
|
18
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Mark `members` as the instance a host's composite produced, and return it
|
|
23
|
+
* unchanged.
|
|
24
|
+
*
|
|
25
|
+
* chant#3608 — every declarable member, and every member of a nested host
|
|
26
|
+
* instance, is also stamped with the composite, the way `expandComposite`
|
|
27
|
+
* stamps a chant instance's members. A destructured member is never expanded,
|
|
28
|
+
* so this is the only record it has that a composite wrote it, and without
|
|
29
|
+
* it a path the fold did not attribute reads as authored and so `direct`.
|
|
30
|
+
* `setProvenance` keeps the first writer, and a nested instance was marked
|
|
31
|
+
* when its own call returned, so a member keeps its innermost composite.
|
|
32
|
+
*/
|
|
33
|
+
export function markHostCompositeInstance(
|
|
34
|
+
members: Record<string, unknown>,
|
|
35
|
+
compositeName: string,
|
|
36
|
+
): Record<string, unknown> {
|
|
37
|
+
if (Object.isExtensible(members) && !Object.prototype.hasOwnProperty.call(members, HOST_COMPOSITE_INSTANCE)) {
|
|
38
|
+
Object.defineProperty(members, HOST_COMPOSITE_INSTANCE, {
|
|
39
|
+
value: compositeName,
|
|
40
|
+
enumerable: false,
|
|
41
|
+
configurable: true,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
stampHostCompositeMembers(members, compositeName);
|
|
45
|
+
return members;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Stamp `composite` on each declarable member of a host instance, descending into nested host instances. */
|
|
49
|
+
function stampHostCompositeMembers(members: Record<string, unknown>, compositeName: string): void {
|
|
50
|
+
for (const member of Object.values(members)) {
|
|
51
|
+
if (isDeclarable(member)) setProvenance(member, { composite: compositeName });
|
|
52
|
+
else if (hostCompositeName(member) !== undefined) stampHostCompositeMembers(member as Record<string, unknown>, compositeName);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The composite name a host composite instance was marked with, or `undefined` for any other value. */
|
|
57
|
+
export function hostCompositeName(value: unknown): string | undefined {
|
|
58
|
+
if (!isPlainIndexable(value)) return undefined;
|
|
59
|
+
const name = (value as Record<symbol, unknown>)[HOST_COMPOSITE_INSTANCE];
|
|
60
|
+
return typeof name === "string" ? name : undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* chant#3598, chant#3612 — a host composite instance's members as entities,
|
|
65
|
+
* for `collectEntities`.
|
|
66
|
+
*
|
|
67
|
+
* `collectEntities` is the one place `build()` and `foldProject` turn exports
|
|
68
|
+
* into entities, so expanding here is what makes the two report the same set:
|
|
69
|
+
* `export const archive = Archive({...})` gives `archiveBucket` to both.
|
|
70
|
+
*
|
|
71
|
+
* The members are named and stamped the way `expandComposite` names and stamps
|
|
72
|
+
* a chant instance: `${exportName}${Member}`, a nested host instance expanded
|
|
73
|
+
* in place under its member's name (`siteMainBucket`), with a whole-entity
|
|
74
|
+
* `composite` origin under the paths the fold already attributed. That origin
|
|
75
|
+
* is written before `collectEntities` adds its own record, and the first
|
|
76
|
+
* writer wins, so a path the fold did not attribute is `unknown` rather than
|
|
77
|
+
* `direct`. Every member belongs to the top-level `instance`; its composite is
|
|
78
|
+
* the innermost one, which {@link markHostCompositeInstance} already stamped.
|
|
79
|
+
*/
|
|
80
|
+
export function expandHostComposite(
|
|
81
|
+
prefix: string,
|
|
82
|
+
members: Record<string, unknown>,
|
|
83
|
+
instance: string = prefix,
|
|
84
|
+
): Array<[string, Declarable]> {
|
|
85
|
+
const out: Array<[string, Declarable]> = [];
|
|
86
|
+
const compositeName = hostCompositeName(members) as string;
|
|
87
|
+
for (const [memberName, member] of Object.entries(members)) {
|
|
88
|
+
if (memberName.length === 0) continue;
|
|
89
|
+
const fullName = `${prefix}${memberName[0].toUpperCase()}${memberName.slice(1)}`;
|
|
90
|
+
if (hostCompositeName(member) !== undefined) {
|
|
91
|
+
out.push(...expandHostComposite(fullName, member as Record<string, unknown>, instance));
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (!isDeclarable(member)) continue;
|
|
95
|
+
setProvenance(member, { composite: compositeName, compositeInstance: instance });
|
|
96
|
+
const composite = getProvenance(member)?.composite ?? compositeName;
|
|
97
|
+
setPathProvenance(member, "", { kind: "composite", composite, instance });
|
|
98
|
+
out.push([fullName, member]);
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
package/src/discovery/index.ts
CHANGED
|
@@ -143,6 +143,16 @@ export interface DiscoveryOptions {
|
|
|
143
143
|
*/
|
|
144
144
|
lexicons?: readonly string[];
|
|
145
145
|
|
|
146
|
+
/**
|
|
147
|
+
* chant#3612 — package specifiers to follow a bare import into, verbatim,
|
|
148
|
+
* alongside whatever {@link lexicons} names. The same allowlist
|
|
149
|
+
* `FoldProjectOptions.lexiconPackages` gives `foldProject` (chant#2438), so
|
|
150
|
+
* a build over a host of its own folds the files `foldProject` folds. A
|
|
151
|
+
* chant project never needs it: its packages are `lexicons`. Only
|
|
152
|
+
* meaningful when {@link fold} is set. Default: none.
|
|
153
|
+
*/
|
|
154
|
+
lexiconPackages?: readonly string[];
|
|
155
|
+
|
|
146
156
|
/**
|
|
147
157
|
* chant #1045 Phase 2 — opt-in: whatever would otherwise reach the
|
|
148
158
|
* in-process `importModule` step (every file, when {@link fold} isn't set;
|
|
@@ -289,7 +299,7 @@ export async function discover(path: string, options?: DiscoveryOptions): Promis
|
|
|
289
299
|
buildParamValuesMap,
|
|
290
300
|
options.lexicons,
|
|
291
301
|
options.sandbox === true,
|
|
292
|
-
[],
|
|
302
|
+
options.lexiconPackages ?? [],
|
|
293
303
|
options.executing === true,
|
|
294
304
|
files,
|
|
295
305
|
)
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* rule because `@intentius/chant/op/gate` pulls in zod and the git ledger
|
|
7
7
|
* code. `@intentius/chant/gate-decision` holds the decision for a plan digest
|
|
8
8
|
* under `ledger`, `pr-review` and `sealed`, the seal check over
|
|
9
|
-
* node:crypto, and the `identity.gates` rule with its class check (#3862). This bundles it the way terragucci would and reads esbuild's
|
|
9
|
+
* node:crypto, and the `identity.gates` rule with its class check (#3862), reading the classes pinned packages supply as data, without ajv (#3888). This bundles it the way terragucci would and reads esbuild's
|
|
10
10
|
* own list of what went in.
|
|
11
11
|
*/
|
|
12
12
|
|
|
@@ -15,7 +15,7 @@ import { build } from "esbuild";
|
|
|
15
15
|
import { describe, expect, test } from "vitest";
|
|
16
16
|
import { REPO } from "./workspace/__fixtures__/contract-repo";
|
|
17
17
|
|
|
18
|
-
const FORBIDDEN = [/node_modules\//, /\/src\/fold\//, /\/src\/lint\//, /\/src\/codegen\//, /\/src\/lifecycle\/git\.ts$/, /\/src\/lifecycle\/gate-ledger\.ts$/];
|
|
18
|
+
const FORBIDDEN = [/node_modules\//, /\/src\/workspace\/(kinds|principal-classes|pin-integrity)\.ts$/, /\/src\/fold\//, /\/src\/lint\//, /\/src\/codegen\//, /\/src\/lifecycle\/git\.ts$/, /\/src\/lifecycle\/gate-ledger\.ts$/];
|
|
19
19
|
|
|
20
20
|
describe("the gate-decision bundle", () => {
|
|
21
21
|
test("holds the decision, the seal check and the plan digest, and no package or git code", async () => {
|
|
@@ -46,13 +46,15 @@ describe("the gate-decision bundle", () => {
|
|
|
46
46
|
"packages/core/src/notify.ts",
|
|
47
47
|
"packages/core/src/op/gate-point.ts",
|
|
48
48
|
"packages/core/src/op/op-waves.ts",
|
|
49
|
+
"packages/core/src/wave-layout.ts",
|
|
50
|
+
"packages/core/src/workspace/class-data.ts",
|
|
49
51
|
"packages/core/src/workspace/identity-gate-rule.ts",
|
|
50
52
|
"packages/core/src/workspace/jsonc.ts",
|
|
51
53
|
"packages/core/src/workspace/trust/gate-seal.ts",
|
|
52
54
|
"packages/core/src/workspace/trust/trust-files.ts",
|
|
53
55
|
]);
|
|
54
|
-
// About
|
|
56
|
+
// About 43 KB unminified today, 7 KB of it the pinned classes read as data (#3888).
|
|
55
57
|
const bytes = result.outputFiles.reduce((n, f) => n + f.contents.byteLength, 0);
|
|
56
|
-
expect(bytes).toBeLessThan(
|
|
58
|
+
expect(bytes).toBeLessThan(48 * 1024);
|
|
57
59
|
});
|
|
58
60
|
});
|