Skip to content

refactor: extract @hyperframes/lint from core#1756

Merged
miguel-heygen merged 6 commits into
mainfrom
refactor/extract-lint
Jun 27, 2026
Merged

refactor: extract @hyperframes/lint from core#1756
miguel-heygen merged 6 commits into
mainfrom
refactor/extract-lint

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

Extracts the project-level linter — lintProject, the rule engine, and shouldBlockRender — out of @hyperframes/core/src/lint/ into a new @hyperframes/lint package.

This is the payoff PR for #1749: a Node app (e.g. an agent harness) can now import { lintProject } from "@hyperframes/lint" and lint a composition directory directly, instead of spawning npx hyperframes lint and parsing stdout.

Part 2 of 3 — stacks on #1755 (@hyperframes/parsers). Merge A first.

What moves

New package source ~4,800 LOC
Tests carried over 257 passing
Files in new package 29

The entire linter moves to @hyperframes/lint — the rule engine (hyperframeLinter, all 9 rules/*, context, types, utils) and the project-orchestration layer (lintProject, directory walking, the render-gate decision). It's the single source of truth: core's compiler render-gate (staticGuard.ts) and the studio preview both consume the rule engine from @hyperframes/lint, so there's no second copy that can drift.

Earlier revisions of this PR left a byte-identical copy of the rule engine behind in packages/core/src/lint/ (used by staticGuard) alongside the new one in @hyperframes/lint (used by the studio preview) — two paths into "the linter" that would silently diverge on the first one-sided rule fix. That dual copy has been removed; staticGuard now imports from @hyperframes/lint directly. (Thanks to the reviewer for catching this.)

API change worth calling out

lintProject now takes a directory string instead of a resolved project object:

- lintProject({ dir, name, indexPath })   // old: CLI-shaped object
+ lintProject(dir)                          // new: just a path

That's deliberate — it drops the CLI's Project type from the public surface so the function is callable from any Node context with nothing but a path. All in-repo callers (lint, preview, publish, render commands) and tests are updated.

Bundle footprint

Artifact Size
dist/ (unpacked) 448 KB
npm tarball (packed) 104 KB (4 files)

How @hyperframes/core changes

packages/core/src/lint/index.ts becomes a one-line @deprecated re-export stub:

/** @deprecated Import from @hyperframes/lint */
export * from "@hyperframes/lint";

So existing @hyperframes/core/lint imports keep working, but new code points at the dedicated package. @hyperframes/lint depends on @hyperframes/core + @hyperframes/parsers + postcss.

Note: lintHyperframeHtml is no longer re-exported from core's main entry (@hyperframes/core) — only from the @hyperframes/core/lint subpath stub and from @hyperframes/lint. Re-exporting it from the main entry would cycle core's index through the lint package (which imports core utilities back); keeping it behind the subpath stub matches how every other extracted package is surfaced.

Test plan

  • bun run --filter @hyperframes/lint test — 257 tests pass
  • bun run --cwd packages/cli test — 1045 tests pass (callers updated for the new signature)
  • bun run build — full monorepo build succeeds
  • Fallow audit passes on CI

miguel-heygen commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@miguel-heygen miguel-heygen force-pushed the refactor/extract-lint branch 3 times, most recently from f16620a to 89fd76a Compare June 27, 2026 03:15

@terencecho terencecho left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pass on the API change is clean. One substantive finding worth resolving before merge — the PR description doesn't quite match the diff, and the divergence it conceals is a real footgun.

The thing. Description says "HTML-level primitives stay in core (they're shared with the compiler); what moves out is the project orchestration layer." True for lintProject and friends, but the entire rule engine — packages/core/src/lint/hyperframeLinter.ts, rules/{adapters,captions,core,gsap,media,textures,composition,fonts,slideshow}.ts, plus every *.test.ts — is still present in core and now also exists in packages/lint/src/ as a byte-near-identical copy (only diff is import-path rewrites in composition.ts / fonts.ts / slideshow.ts to consume @hyperframes/parsers / @hyperframes/core/fonts/aliases / @hyperframes/core/slideshow).

That gives the runtime two paths into "the linter":

  • core's compiler at packages/core/src/compiler/staticGuard.ts:1 does import { lintHyperframeHtml } from "../lint/hyperframeLinter" — the local copy.
  • studio's preview at packages/studio/vite.adapter.ts:193 does server.ssrLoadModule("@hyperframes/core/lint")@hyperframes/core/src/lint/index.ts stub → @hyperframes/lint — the other copy.

ALL_RULES at packages/lint/src/hyperframeLinter.ts:14-24 is the same 9-array spread as core's at packages/core/src/lint/hyperframeLinter.ts:14-24. Byte-identical today, which is exactly when this risk is invisible — the first rule fix that lands in only one diverges the studio preview from the render-time render-gate guard.

Two reasonable resolutions:

  1. Pick a direction: re-point staticGuard.ts at @hyperframes/lint, delete packages/core/src/lint/{rules,hyperframeLinter,context,types,utils}.ts (and their tests, which are also duplicated). The deprecated stub at packages/core/src/lint/index.ts already routes everything else through.
  2. Document the dual-copy contract explicitly in both hyperframeLinter.ts files and staticGuard.ts so a future maintainer is forced to update both copies in lockstep.

(1) is cleaner; (2) is fast.

Everything else verified clean

  • Signature break — fully migrated. lintProject(projectDir: string) at packages/lint/src/project.ts:168. All 4 in-CLI callers updated: commands/lint.ts:41 (project.dir), commands/preview.ts:129 (dir), commands/publish.ts:38 (dir), commands/render.ts:741 (project.dir). studioServer.ts doesn't call lintProject. Tests in packages/cli/src/utils/lintProject.test.ts updated. No stray { dir, name, indexPath } shape on this branch.
  • Behavioral parity. Old lintProject consumed project.indexPath + project.dir; project.name was always caller-only. New project.ts:169 derives indexPath via resolve(projectDir, "index.html"). Same outcome.
  • External-consumer surface. Only two @hyperframes/core/lint subpath consumers in the repo (cli/src/utils/lintFormat.test.ts:2, studio/vite.adapter.ts:193); both resolve cleanly through the stub. No @hyperframes/core/lint/rules / /shouldBlockRender subpath exports in core/package.json — so no other subpath imports could've existed.
  • Stack-base: gh pr diff 1756 touches 51 files, zero in packages/parsers/ — no #1755 bleed.
  • Rule registration: ALL_RULES spreads all 9 rule arrays identically in both copies — no rule silently dropped during the move.

Not approving yet purely on the dual-engine point — happy to flip to approve once that's either resolved or explicitly documented.

— Review by tai (pr-review)

@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

Thanks for the dual-engine catch — that was a real footgun. Went with resolution (1): deleted core's copy of the rule engine (packages/core/src/lint/{rules/*,hyperframeLinter,context,types,utils}.ts + their tests) and re-pointed compiler/staticGuard.ts at @hyperframes/lint. The render-gate and the studio preview now share one rule engine; the @hyperframes/core/lint stub keeps back-compat.

One follow-on: re-exporting lintHyperframeHtml from core's main entry would cycle core's index through the lint package (lint imports core utils back — fallow flagged it), so it's now surfaced only via the @hyperframes/core/lint subpath stub and @hyperframes/lint — consistent with how the other extracted packages are exposed. Updated the stale main-entry export test accordingly, and fixed the description wording you flagged.

refactor(core): single-source the lint engine — 8,627 LOC of duplication removed. Green locally: core 1114, lint 257, cli 1045, fallow audit passes.

terencecho
terencecho previously approved these changes Jun 27, 2026

@terencecho terencecho left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified the dual-engine point is resolved at f86aa6b:

  • packages/core/src/compiler/staticGuard.ts:1 now imports lintHyperframeHtml from @hyperframes/lint directly — the local re-import is gone.
  • packages/core/src/lint/ is empty except for the 84-byte @deprecated re-export stub at index.ts. All the duplicated rule files (hyperframeLinter.ts, rules/*.ts, context.ts, types.ts, utils.ts, plus tests) are gone from core.
  • Bonus rewire on packages/producer/src/services/hyperframeLint.ts:3 — that consumer also picks up from @hyperframes/lint directly rather than going through the stub. Nice.

Single source of truth for the rule engine, with the stub catching any straggler @hyperframes/core/lint imports through the deprecation window. No way for the studio preview and render-gate to diverge.

UNSTABLE merge state is just the regression shards + perf shards + Graphite mergeability check still spinning — none gating on this change shape.

— Review by tai (pr-review)

@miguel-heygen miguel-heygen changed the base branch from refactor/extract-parsers to graphite-base/1756 June 27, 2026 04:46
Moves all lint rules, hyperframeLinter, lintProject, and related types
from packages/core/src/lint/ into a new standalone packages/lint package.

Core keeps a thin re-export stub at @hyperframes/core/lint for backward
compatibility. Consumer imports (cli lint command, producer hyperframeLint)
are updated to import from @hyperframes/lint directly.

Depends on @hyperframes/parsers (PR #1755).
@miguel-heygen miguel-heygen force-pushed the refactor/extract-lint branch from f86aa6b to d47539e Compare June 27, 2026 04:46
@graphite-app graphite-app Bot changed the base branch from graphite-base/1756 to main June 27, 2026 04:47
@graphite-app graphite-app Bot dismissed terencecho’s stale review June 27, 2026 04:47

The base branch was changed.

Delete core's byte-identical copy of the lint rule engine and re-point
staticGuard at @hyperframes/lint, so the render-time render-gate and the
studio preview share one rule engine instead of two copies that could
silently diverge. Back-compat preserved via the @hyperframes/core/lint stub.

Addresses review feedback on the dual-copy footgun.
@miguel-heygen miguel-heygen force-pushed the refactor/extract-lint branch from d47539e to 3c46e38 Compare June 27, 2026 04:47
@miguel-heygen miguel-heygen merged commit 98d0bdd into main Jun 27, 2026
123 of 129 checks passed
@miguel-heygen miguel-heygen deleted the refactor/extract-lint branch June 27, 2026 05:16
WaterrrForever added a commit that referenced this pull request Jun 27, 2026
The lint extraction (#1756) made @hyperframes/lint a runtime dependency of
core — core's compiled compiler/staticGuard.js imports it via the package's
"node" export condition (./dist/index.js). But the Test and Studio-load-smoke
jobs pre-build only @hyperframes/{parsers,studio-server} before packages/core,
so loading core's dist at test / dev-server time fails with:

  ERR_MODULE_NOT_FOUND: Cannot find module .../@hyperframes/lint/dist/index.js
  imported from .../packages/core/dist/compiler/staticGuard.js

Build the canonical pre-core set @hyperframes/{parsers,lint,studio-server}
(the glob the root build script uses) in both jobs so it can't drift again.
The SDK job is left as-is — it builds parsers+core only and passes.

Reproduced locally: removing packages/lint/dist reproduces the exact
ERR_MODULE_NOT_FOUND; building lint resolves it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WaterrrForever added a commit that referenced this pull request Jun 27, 2026
…1768)

* fix(cli): always check GitHub skills on init while skills.sh syncs

The "don't pass --skip-skills" guidance lives in SKILL.md, which ships
through the laggy skills.sh registry and can't be relied on to reach the
agent — so an agent that improvises `--skip-skills` silently dodges the
GitHub skills freshness pull. Put the guarantee in the CLI instead (the
one channel that updates promptly via `npx hyperframes@latest`):

- Neuter the `--skip-skills` FLAG so it no longer skips the check; gate
  skipping on the HYPERFRAMES_SKIP_SKILLS=1 env var instead (the
  agent/user CLI path never sets it). Print a one-line notice when the
  ignored flag is passed.
- Wire the env escape hatch into the init test helper (one place) and the
  CI smoke-test / windows-canary steps so they stay offline and fast.
- Update the skill docs that previously told agents `--skip-skills` opts
  out.

Temporary measure while skills.sh catches up — revert init.ts's
`skipSkills` to `args["skip-skills"] === true` once it does (noted inline).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ci): build @hyperframes/lint before core in Test and Studio jobs

The lint extraction (#1756) made @hyperframes/lint a runtime dependency of
core — core's compiled compiler/staticGuard.js imports it via the package's
"node" export condition (./dist/index.js). But the Test and Studio-load-smoke
jobs pre-build only @hyperframes/{parsers,studio-server} before packages/core,
so loading core's dist at test / dev-server time fails with:

  ERR_MODULE_NOT_FOUND: Cannot find module .../@hyperframes/lint/dist/index.js
  imported from .../packages/core/dist/compiler/staticGuard.js

Build the canonical pre-core set @hyperframes/{parsers,lint,studio-server}
(the glob the root build script uses) in both jobs so it can't drift again.
The SDK job is left as-is — it builds parsers+core only and passes.

Reproduced locally: removing packages/lint/dist reproduces the exact
ERR_MODULE_NOT_FOUND; building lint resolves it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cli): address PR #1768 review — stale comment + harden offline init

- Update the stale interactive-path comment that still said "Opt out with
  --skip-skills"; the flag is neutered, opt-out is HYPERFRAMES_SKIP_SKILLS=1.
- Wrap installAllSkills in ensureSkillsCurrent with try/catch. installAllSkills
  is already non-strict (swallows its own failures), but since --skip-skills no
  longer escapes this path, every init — including offline ones that fall through
  to "install anyway" — runs it. The guard guarantees a skills-install failure
  only warns and proceeds, never breaks init.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
miguel-heygen added a commit that referenced this pull request Jun 27, 2026
New docs pages for the three packages extracted from core (#1755, #1756,
#1757), wired into the Packages nav after @hyperframes/core. Each covers
when-to-use, exports, and API with cross-links. Core's parser/lint sections
now point at the dedicated packages and its Related Packages lists all three.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants