Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export default defineConfig({
{ label: 'Choosing Your Deployment Model', slug: 'concepts/deployment-paths' },
{ label: 'Components', slug: 'concepts/components' },
{ label: 'TypeScript as Data', slug: 'concepts/typescript-as-data' },
{ label: 'Left of the Line', slug: 'concepts/left-of-line' },
{ label: 'Build-Time Parameters', slug: 'concepts/build-time-parameters' },
{ label: 'Where Values Come From', slug: 'concepts/where-values-come-from' },
{ label: 'Evaluation Pipeline', slug: 'concepts/evaluation-pipeline' },
Expand Down
5,876 changes: 5,876 additions & 0 deletions docs/public/leftness/cdk-synth.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1,913 changes: 1,913 additions & 0 deletions docs/public/leftness/chant-build-fold.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
54 changes: 54 additions & 0 deletions docs/src/content/docs/concepts/left-of-line.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
title: Left of the Line
description: A measured comparison — what chant and CDK each know about your infrastructure before running your code
---

chant claims it knows a project's resource graph before executing any of the project's code. CDK cannot make that claim, and the reason is structural rather than a gap in its tooling: mutation after construction, aspects, escape hatches, and jsii mean there is no static path from a CDK app to its graph. `cdk synth` learns what your infrastructure is by running your program.

That difference is usually asserted in prose. This page shows it as a measurement, taken by one procedure that runs identically against both tools.

## The procedure

The same infrastructure — a CRUD items API (DynamoDB table, Lambda handler, HTTP API with four routes, and the IAM the wiring implies) — expressed twice, once as chant declarables and once as a plain-JavaScript CDK app. A parity check keeps the pair honest: the resource surfaces are identical apart from two documented expansion-style differences, both of which make the CDK template larger, never the reverse.

Both synths run under `node --cpu-prof`. On the CDK side the capture profiles the app subprocess, not the CLI — `cdk synth` spawns your program, so profiling the CLI would measure the wrong process, and the app-only capture is the version CDK has no grounds to object to. One analyzer then asks both captures the same question:

**Did any frame from the project's own source files execute?**

| | `chant build --fold` | `cdk synth` (app process) |
| --- | --- | --- |
| project code executed | **no** | **yes** |
| third-party code executed because the definitions ran | 0 MB | ~1.8 MB (`aws-cdk-lib` + its closure) |
| synthesizer machinery executed | ~10.9 MB (chant, TypeScript parser, tsx) | not in this capture (the CLI is a separate process) |

Timing appears nowhere in this table on purpose. CDK's profile is wider — bundling, asset hashing, jsii startup — but all of that happens after the boundary, so none of it moves these numbers. This is not a benchmark, and if it ever reports which tool is faster the metric has drifted.

The chant "no" is not just an absence of sampled frames, which a sampling profiler could in principle miss. The capture run fails unless every file in the estate reports `[fold:fold]` — zero module execution, enforced by the build itself, not inferred from samples. A sampler can miss a frame; it cannot invent one, so the CDK "yes" is definitive on its own.

## The same fact, rendered

Both captures below are timelines: the x-axis is time, so *left* literally means *before*. Frames are colored by package. Frames belonging to the project's own source files are highlighted; everything else is dimmed.

The CDK capture lights up, and a marker lands on the first project frame near the start of the run — everything CDK learns about the infrastructure comes after that line:

![cdk synth timeline — project frames highlighted, first project frame marked near t=0](/leftness/cdk-synth.svg)

[Explore the CDK capture interactively](https://spicypath.intentius.workers.dev/leftness-cdk) — the link restores the timeline, the highlight, and the marker.

The chant capture is a busy timeline — the TypeScript parser is doing real work — with an empty highlight. There is no marker because there is no project frame to mark:

![chant build --fold timeline — zero project functions matched, no project frame exists to mark](/leftness/chant-build-fold.svg)

[Explore the chant capture interactively](https://spicypath.intentius.workers.dev/leftness-chant) — same search, visibly 0 matches — the strip dims because nothing qualifies.

A blank flame graph would look like a broken file. A full one with nothing to highlight is the accurate picture: the tool works, your code never runs.

## Capability and delivery, side by side

The measurement above is a capability claim about code inside chant's supported subset: the harness estate folds 5 of 5 files, so the boolean holds by construction. It is not yet a delivery claim about arbitrary real applications. On loomster, the largest real application built on chant, no resource-bearing infrastructure file folds today — the blockers are ordinary authoring patterns (exported helper functions and their import closure) that the fold subset deliberately excludes. Those files run in a sandboxed child instead; the numbers on this page describe the folded path.

Both facts belong in the same paragraph. chant is left of the line for what folds, the fold subset is growing, and the honest way to read this page is: the boundary exists by construction, and how much of your application lives left of it depends on how much of it folds.

## Reproducing

Everything regenerates from committed inputs — pinned package versions, lockfiles, and the raw `.cpuprofile` captures live in [`test/leftness/`](https://github.com/INTENTIUS/chant/tree/main/test/leftness) with the procedure in its README. `just leftness-capture` runs the whole thing offline; no cloud credentials are involved. The timeline renderings are static SVG exports from [spicypath](https://github.com/INTENTIUS/spicypath), regenerable with `node exhibits/render.mjs`.
4 changes: 4 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ gitlab-runtime-e2e:
forgejo-runtime-e2e:
bash test/forgejo-runtime-e2e.sh

# Left-of-line proof capture (#1084): profile chant build --fold vs cdk synth on the matched pair, one measurement on both (on-demand, needs network for pinned installs; no cloud credentials)
leftness-capture:
bash test/leftness/capture.sh

# Deploy the components-aws-e2e example against a local AWS emulator (Floci in Docker; on-demand, needs Docker + aws CLI)
components-aws-e2e:
bash test/components-aws-e2e.sh
Expand Down
4 changes: 4 additions & 0 deletions test/leftness/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
chant-app/node_modules/
chant-app/out/
cdk-app/node_modules/
cdk-app/cdk.out/
81 changes: 81 additions & 0 deletions test/leftness/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Left-of-line proof harness (#1084)

chant's epic #1019 claims the tool is left of the line by construction: it knows a
project's resource graph before executing any of the project's code. This directory makes
that a measurement instead of a sentence — one procedure, run identically against a tool
that structurally cannot pass it.

## The matched pair

The same infrastructure, expressed twice: a CRUD items API (DynamoDB table, Lambda
handler, HTTP API with four routes, and the IAM the wiring implies). The shape is the
canonical CDK CRUD example, sized big enough to be honest and small enough to audit.

- `chant-app/` — chant declarables against pinned published packages. Declares explicitly
what CDK implies (the execution role's policy, the API's invoke permission).
- `cdk-app/` — plain-JavaScript CDK app, deliberately: no ts-node or bundler frames muddy
the capture. `cdk.json` profiles the **app subprocess**, not the CLI — `cdk synth`
spawns the app, so profiling the CLI would measure the wrong process, and the app-only
capture is the version CDK has no grounds to object to.

`parity.mjs` holds the pair together: identical resource surface, with exactly two
documented expansion-style deltas (CDK's separate `IAM::Policy`; CDK's per-route
`Lambda::Permission` × 4 vs one wildcard permission), both of which make the CDK template
larger, never the reverse. Any other drift fails the run.

## The measurement

`./capture.sh` (or `just leftness-capture` from the repo root) installs both estates from
committed lockfiles, profiles both synths with `node --cpu-prof`, and applies one
analyzer to both captures. No cloud credentials; both synths are offline. Timing is
deliberately absent from every output — this is not a benchmark, and if it ever reports
which tool is faster the metric has drifted (#1084's non-goal).

Two numbers per tool, boundary definitions in `analyze.mjs`'s header:

1. **Project-code boolean** — did any frame from the estate's own source files execute?
A cpuprofile cannot say what fraction of the graph was known at time T (the original
AC asked for a measurement a capture cannot carry), so the headline is the boolean,
which is the strong claim anyway. Sampling can miss frames but never invent them, so
the CDK side's `true` is definitive; the chant side's `false` is additionally pinned
by an unsampled invariant — the run fails unless every file reports `[fold:fold]`,
zero module execution, enforced by the build itself.
2. **Trusted-computing-base bytes** — distinct third-party files observed executing,
byte-sized on disk. Each estate's `package.json` declares exactly one kind of
dependency, so the role split (synthesizer vs definition library) falls out of the
manifest, not a curated list. Both tools execute machinery; only one must execute the
project's definition graph to know what it builds.

## Current committed result

| measurement | chant build --fold | cdk synth (app) |
| --- | --- | --- |
| project code executed | **false** | **true** |
| definition-library TCB | 0 MB | ~1.7 MB |
| synthesizer TCB | ~10.8 MB | 0 MB (CLI not in the app capture) |

`captures/` holds the raw `.cpuprofile`s (path prefixes sanitized to `/leftness` so the
files are machine-independent; regenerating rewrites them). `results/` holds the analyzer
and parity outputs.

## Exhibits

`exhibits/render.mjs` turns both captures into static timeline SVGs via spicypath's
headless renderers (package-attributed colors, search-dim on the estate's own files, a
derived marker at the first project frame):

```
node exhibits/render.mjs /path/to/spicypath
```

The CDK exhibit carries a "first project frame" marker near t=0. The chant exhibit has no
project frame to mark, and its title says `0 project fns matched` — a busy timeline (the
TypeScript parser is real work) with an empty highlight. That asymmetry is the argument,
rendered.

## Reproducing

Everything regenerates from committed inputs: pinned exact versions in both estates'
`package.json` + committed lockfiles, `capture.sh` end to end. Machine-local absolute
paths never survive into the committed captures. Sampling profiles vary run to run in
which frames they catch; neither reported number depends on sample timing.
140 changes: 140 additions & 0 deletions test/leftness/analyze.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// One measurement, applied identically to both captures (chant #1084).
//
// A .cpuprofile records which functions were on a sampled stack. Two things are derived
// from each capture, with the boundary definitions below stated precisely enough to argue
// with:
//
// 1. PROJECT-CODE BOOLEAN — did any frame from the estate's own source files execute?
// "Project" = a file inside the estate directory that is not under node_modules.
// This is the honest form of the headline (see #1084's correction comment): a profile
// cannot say what fraction of the graph was known at time T, but it can say whether
// the tool ever ran your code at all. Sampling can only miss frames, never invent
// them — so a "yes" is definitive, and the chant side's "no" is additionally pinned
// by a hard, unsampled invariant: capture.sh fails unless every file reported
// [fold:fold] (zero module execution, enforced by the build itself).
//
// 2. TRUSTED-COMPUTING-BASE BYTES — bytes of distinct third-party files observed
// executing (every node_modules file appearing in the capture, byte size on disk,
// each file counted once). Split two ways per tool, and the split is NOT hand-curated:
// each estate's package.json declares exactly one kind of dependency, so every
// observed third-party package inherits that estate's role.
// - chant-app declares only the SYNTHESIZER (@intentius/chant + lexicon); tsx,
// typescript, zod, js-yaml etc. are its transitive machinery. The CDK CLI never
// appears on the other side because the app subprocess is what's profiled.
// - cdk-app declares only the DEFINITION LIBRARY (aws-cdk-lib + constructs);
// minimatch, semver, @aws-cdk/* are its transitive closure — code that runs
// BECAUSE the project's definitions run.
// Both tools execute machinery; only one must execute the project's definition
// graph to know what it builds. That asymmetry is what the split shows.
//
// Timing is deliberately absent from the output (#1084's non-goal: not a benchmark).

import { readFileSync, writeFileSync, statSync, readdirSync } from "node:fs";
import { join, resolve } from "node:path";

const HERE = resolve(".");

// Per-tool boundary config. `role` is the classification every observed third-party
// package inherits — it follows from what the estate's package.json declares (see the
// module doc above), not from a curated allowlist.
const TOOLS = {
chant: {
capture: "captures/chant-build.cpuprofile",
estate: resolve("chant-app"),
role: "synthesizer",
},
cdk: {
capture: "captures/cdk-app.cpuprofile",
estate: resolve("cdk-app"),
role: "definition-library",
},
};

function stripFileUrl(u) {
return u.startsWith("file://") ? decodeURIComponent(u.slice("file://".length)) : u;
}

// Innermost node_modules segment → package name (@scope kept with its name).
function packageOf(path) {
const segs = path.split("/");
const nm = segs.lastIndexOf("node_modules");
if (nm < 0 || !segs[nm + 1]) return null;
const pkg = segs[nm + 1];
return pkg.startsWith("@") && segs[nm + 2] ? `${pkg}/${segs[nm + 2]}` : pkg;
}

function analyze(name, cfg) {
const prof = JSON.parse(readFileSync(cfg.capture, "utf8"));
const files = new Set();
for (const n of prof.nodes) {
const url = n.callFrame?.url;
if (url) files.add(stripFileUrl(url));
}

const projectFiles = [];
const thirdParty = new Map(); // package → { files: Set, bytes }
let nodeInternal = 0;

for (const f of files) {
if (f.startsWith("node:")) { nodeInternal++; continue; }
const pkg = packageOf(f);
if (pkg) {
let e = thirdParty.get(pkg);
if (!e) thirdParty.set(pkg, (e = { files: new Set(), bytes: 0 }));
if (!e.files.has(f)) {
e.files.add(f);
try { e.bytes += statSync(f).size; } catch { /* sanitized/relocated capture: bytes need a live tree */ }
}
continue;
}
if (f.startsWith(cfg.estate + "/") || f === join(cfg.estate, "app.js")) projectFiles.push(f);
}

const total = [...thirdParty.values()].reduce((a, e) => a + e.bytes, 0);
const pkgTable = [...thirdParty.entries()]
.map(([pkg, e]) => ({ pkg, files: e.files.size, bytes: e.bytes, role: cfg.role }))
.sort((a, b) => b.bytes - a.bytes);

return {
tool: name,
capture: cfg.capture,
projectCodeExecuted: projectFiles.length > 0,
projectFilesObserved: projectFiles.map((f) => f.slice(cfg.estate.length + 1)).sort(),
trustedComputingBase: {
synthesizerBytes: cfg.role === "synthesizer" ? total : 0,
definitionLibraryBytes: cfg.role === "definition-library" ? total : 0,
packages: pkgTable,
},
nodeInternalModulesObserved: nodeInternal,
};
}

const results = {};
for (const [name, cfg] of Object.entries(TOOLS)) results[name] = analyze(name, cfg);

// The chant side's boolean must agree with the fold log's hard invariant.
const foldLog = readFileSync("results/chant-build.log", "utf8");
const foldRuns = (foldLog.match(/\[fold:run\]/g) || []).length;
results.invariants = {
chantFoldRunCount: foldRuns,
agreement: results.chant.projectCodeExecuted === false && foldRuns === 0
? "capture and fold log agree: zero project-code execution on the chant side"
: "DISAGREEMENT — investigate before trusting either number",
};

writeFileSync("results/analysis.json", JSON.stringify(results, null, 2) + "\n");

const mb = (b) => (b / 1024 / 1024).toFixed(2) + " MB";
console.log("\n measurement chant cdk");
console.log(` project code executed ${String(results.chant.projectCodeExecuted).padEnd(20)} ${results.cdk.projectCodeExecuted}`);
console.log(` definition-library TCB ${mb(results.chant.trustedComputingBase.definitionLibraryBytes).padEnd(20)} ${mb(results.cdk.trustedComputingBase.definitionLibraryBytes)}`);
console.log(` synthesizer TCB ${mb(results.chant.trustedComputingBase.synthesizerBytes).padEnd(20)} ${mb(results.cdk.trustedComputingBase.synthesizerBytes)}`);
console.log(` ${results.invariants.agreement}\n`);
if (results.chant.projectCodeExecuted) {
console.error("FAIL: project frames observed in the chant capture:", results.chant.projectFilesObserved);
process.exit(1);
}
if (!results.cdk.projectCodeExecuted) {
console.error("FAIL: no project frames in the CDK capture — the app was not what got profiled.");
process.exit(1);
}
58 changes: 58 additions & 0 deletions test/leftness/capture.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/bin/bash
# Left-of-line capture (chant #1084): profile both tools producing their output with
# node --cpu-prof, then apply ONE measurement to both captures (analyze.mjs).
#
# Run from this directory: ./capture.sh (or `just leftness-capture` from the repo root)
# Needs: node >= 22, network for the two pinned npm installs. No cloud credentials —
# both synths are offline. Deliberately sequential; nothing here is timing-sensitive
# because the measurement excludes timing by design.
set -euo pipefail
cd "$(dirname "$0")"
HERE="$(pwd)"

echo "── install (pinned, committed lockfiles)"
(cd chant-app && npm ci --no-audit --no-fund 2>/dev/null || npm install --no-audit --no-fund)
(cd cdk-app && npm ci --no-audit --no-fund 2>/dev/null || npm install --no-audit --no-fund)

mkdir -p captures results chant-app/out

echo "── capture: chant build --fold (single process, no sandbox — the fold IS the claim)"
# chant's published bin execs `npx tsx`; profiling that would profile npx. Invoke the
# real entry point directly under one profiled node process instead.
(cd chant-app && node --cpu-prof --cpu-prof-dir="$HERE/captures" --cpu-prof-name=chant-build.cpuprofile \
--import tsx node_modules/@intentius/chant/src/cli/main.ts \
build src --lexicon aws --fold -o out/template.json 2> "$HERE/results/chant-build.log")

echo "── assert: every chant-app file folded (the boolean's hard invariant, not sampled)"
FOLDS=$(grep -c '\[fold:fold\]' results/chant-build.log || true)
RUNS=$(grep -c '\[fold:run\]' results/chant-build.log || true)
echo " fold=$FOLDS run=$RUNS"
if [ "$RUNS" != "0" ] || [ "$FOLDS" = "0" ]; then
echo "FAIL: chant estate did not fully fold — the capture would not support the claim." >&2
grep '\[fold:run\]' results/chant-build.log >&2 || true
exit 1
fi

echo "── capture: cdk synth (profiles the APP subprocess via cdk.json, not the CLI)"
(cd cdk-app && npx cdk synth --no-version-reporting --no-path-metadata --no-asset-metadata --quiet > /dev/null)

echo "── analyze: one measurement, both captures"
node analyze.mjs

echo "── parity: the pair is still the same infrastructure"
node parity.mjs

echo "── sanitize: strip machine-local path prefixes from the committed captures"
node -e '
const fs = require("fs");
const here = process.cwd();
for (const f of fs.readdirSync("captures")) {
if (!f.endsWith(".cpuprofile")) continue;
const p = "captures/" + f;
let s = fs.readFileSync(p, "utf8");
s = s.split("file://" + here).join("file:///leftness").split(here).join("/leftness");
fs.writeFileSync(p, s);
}
console.log(" captures sanitized (paths rooted at /leftness)");
'
echo "done — captures/ + results/analysis.json updated"
1 change: 1 addition & 0 deletions test/leftness/captures/cdk-app.cpuprofile

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions test/leftness/captures/chant-build.cpuprofile

Large diffs are not rendered by default.

Loading
Loading