6 Commits

Author SHA1 Message Date
Maxim 4b17ea7d35 fix(scripts): tokenize before hunting loader calls in the purity gate
The #4893 hard-fail gate's loader-call detector gave WRONG VERDICTS IN BOTH
DIRECTIONS. It layered two regexes — a comment/string/template alternation that
blanked only the comment branch, and `\b(?:import|require(?:\.resolve)?)\s*\(`
over the result — then classified an argument as static from the FIRST CHARACTER
after the paren. All nine shapes below were reproduced against the real gate
before the rewrite:

  false FAIL  throw new Error("use require(path) instead")
  false FAIL  `import(${x})` inside a template
  false FAIL  o.import(y) / mod.require(x)          (member calls, not loaders)
  false PASS  /https:\/\//; …import(n)              (the regex's `//` blanked the
                                                     rest of the line, hiding a
                                                     real dynamic call)
  false PASS  import(`stream${n}`)                  (merely STARTS with a quote)
  false PASS  import("zo" + n)                      (same)
  false PASS  import(`${base}/v2/index.mjs`)        (same — the fat entry)
  false PASS  __require(name)                       (no \b inside `__require`)

Replaced with `scanSource`, a single-pass tokenizer that classifies every
character as code / comment / string / template / regex and returns a
length-preserving masked view plus a literal-span list. The one surviving regex
now only ever sees code, so import-shaped TEXT cannot reach it at all; an
argument counts as static only when it is one COMPLETE literal with no
concatenation or interpolation; `__require` is matched; and a member call is
rejected both by lookbehind and by a whitespace-skipping back-scan (so
`m\n  .import(x)` is not a loader either).

Proven in both directions: nine innocent/violation pairs run through the real
`assertEntryPurity`, each innocent form CLEAN and each matching real violation
FAIL. Re-proved end-to-end by prepending `import "streamdown"` to the real
dist/v2/headless.mjs — exit 1 naming all five families — then restoring it
byte-identically. On the untouched dist the scan sees 66 loader calls in the
`.cjs` graph and classifies all 66 static, so it passes because it LOOKED.

Also adds the first `.cjs` fixtures: every existing fixture was `.mjs`, leaving
the script's `format: "cjs"` branch and the `require()` shape asserted by
nothing. Tests 24 → 47.

`stripComments` is renamed `maskNonCode`, since it now blanks literals and
regexes too; it had no caller outside this script and its test. The RN guard
keeps its own copy, untouched.

dev-docs/bundle-size.md: the four holes a sibling agent documented as known
limitations this round are closed and removed from that list; what genuinely
remains (regex-vs-division heuristic, no JSX/TS, indirect loaders) replaces them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 17:27:47 +02:00
Maxim ed087edbf4 docs(bundle-size): describe the purity gate that actually exists
e7f3d7644d rewrote the #4893 gate from a substring scan of four inlined entry
files into an esbuild `metafile` walk over the resolved graph, but the doc was
corrected one commit earlier (2158a6f382) and so described the deleted
implementation.

Rewritten to match the code:

- Tier 3 item 1 now describes the graph assertion: esbuild with `metafile: true`,
  matching on resolved input paths (never file contents), `packageNameFor`'s
  last-`node_modules/` rule, external specifiers collected from
  `imports[].external`, fail-loud on an unresolvable edge / an entry missing from
  its own graph / graph-blinding warnings, comment stripping before the one text
  scan, and the negative tests in scripts/__tests__/assert-headless-purity.test.mjs.
- Dropped the now-false claims: "substring scan of four files", "cannot follow
  edges", "a forbidden name in a comment fails it", and "a dep arriving
  transitively through an externalized package would pass" (the gate re-bundles
  with only react/react-dom external).
- `size:headless` no longer described as never hard-failing: it exits non-zero on
  an unbuilt dist, an esbuild error, and a total of 0 or under
  MIN_PLAUSIBLE_BYTES. `size:headline` keeps its zero-output guard. Phase 1's
  "no hard-fail" is now scoped to size *thresholds*.
- Added a Known limitations list so the doc does not overclaim in the other
  direction: the first-character-after-`(` literal check (a template literal or
  concatenation starting with a quote is skipped), unmatched `__require(...)`,
  string literals not stripped before the text scan, workspace-sibling `dist`
  counted as first-party (so a dep inlined there yields no package name), only
  the four .mjs/.cjs targets asserted, and the `@`-prefix family match's
  over-reach.
- Tier 3 item 2 now reflects that the RN walker also extracts dynamic
  import()/require() and fails loudly on unresolvable local edges.

Workflow claims verified against .github/workflows/static_bundle_size.yml: the
action pin (2.10.0) and the "step names /v2/headless, script covers /v2/context"
note were already correct and are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 17:01:22 +02:00
Maxim 2158a6f382 docs(bundle-size): correct the action pin, package inventory and guard coverage
The Tier 3 section shipped in this PR made claims the code contradicts:

- The action pin read v2.9.1; static_bundle_size.yml pins 2.10.0 (by SHA).
- assert-headless-purity.mjs asserts four targets — dist/v2/headless.{mjs,cjs}
  AND dist/v2/context.{mjs,cjs} — not just the headless pair.
- The package inventory enumerated 9 packages while the workflow glob covers 10:
  react-native was neither listed nor classified, and the "no size script" claim
  was wrong for it (it ships size:headless).
- "Both directions of the #4893 regression" overclaimed. The purity script is a
  substring scan of four emitted files with @copilotkit/core, @copilotkit/shared,
  @ag-ui/*, rxjs, zod and uuid external, so it cannot see a heavy dep arriving
  through an external edge; the RN test never resolves bare specifiers, so it
  cannot enter node_modules. Both limits are now stated, along with the gap
  neither guard covers.
- The heading said "two tiers" while three are documented.

Docs only — no change to the script or the workflow.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 21:38:09 +02:00
Maxim f34b7aa23c ci: measure @copilotkit/react-native bundle size (was absent from the glob)
react-native was missing from static_bundle_size.yml's package glob, so its
dist/ has never been measured despite being the consumer most exposed to the
#4893 regression (Metro does not tree-shake). This adds coverage:

- Extend the compressed-size-action glob to include react-native.
- New scripts/measure-headless.mjs: an esbuild-driven gzip signal for the
  @copilotkit/react-native/headless entry, mirroring react-core's
  measure-copilotchat.mjs (stdin + resolveDir, gzip sum, job-summary output).
- Wire a build + measure step into the copilotchat-import-size CI job.

First baseline: @copilotkit/react-native/headless = 92.8 kB gzip
(esbuild regression signal, not a Metro figure).

No limit fields (Phase 1 policy — see dev-docs/bundle-size.md).

esbuild added as a react-native devDependency (^0.27.0, matching react-core);
the root ">=0.25.4" override keeps the monorepo on a single esbuild (0.27.3).

Two corrections to the drafted script, verified by running it:
- Fed the entry via esbuild stdin with resolveDir=pkgRoot; a temp-dir entry
  cannot resolve @copilotkit/react-native/headless through workspace node_modules.
- Dropped useRenderToolCall from the import list — the RN headless surface
  deliberately does not export it (DOM-dependent; see src/index.ts).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-08 14:51:55 +02:00
Maxim 68a30c2535 test(react-core): hard-fail if the /v2/headless chunk links the render stack (#4893) 2026-08-08 14:36:45 +02:00
David McKay c6ca283e96 feat(ci): bundle-size tracking + ES-compat checks (OSS-123, OSS-121)
Adds two CI signals for keeping the published packages small and broadly compatible:

- Bundle size: size-limit file-mode config across packages plus a
  CopilotChat import-size regression signal (gzip) so growth in the
  headline consumer entrypoint is visible on every PR. A bundle-size
  workflow comments results on the PR (Phase 1: no hard-fail).
- ES compatibility: a compat-check (es-check) script across 9 packages
  with a root .browserslistrc, validating built .mjs/.cjs against the
  es2022 build target.

The measure script is importable (measureBundle) and unit-tested. Dev
docs live under dev-docs/ (bundle-size.md, browser-compat.md). All
action refs are pinned to full commit SHAs for supply-chain safety.
2026-05-29 16:44:35 -07:00