Skip to content

feat(core): validation Phase 4 — generated-source contract check (G5)#497

Merged
laofahai merged 12 commits into
mainfrom
feat/g5-phase4-contract
Jun 8, 2026
Merged

feat(core): validation Phase 4 — generated-source contract check (G5)#497
laofahai merged 12 commits into
mainfrom
feat/g5-phase4-contract

Conversation

@laofahai

@laofahai laofahai commented Jun 7, 2026

Copy link
Copy Markdown
Owner

What

Validation Phase 4 — generated-source CONTRACT check (G5). Replaces the
skipped Phase 4 stub with validatePhase4: a static, execution-free check
that any AI-materialized generatedSource (from the #495 materializer) actually
defines the change's declared target/name — the right define*() call, a
reference to its name, and an import from @linchkit/core. It catches a class of
AI errors that pass the Phase 2 syntax gate yet are wrong (empty scaffold, wrong
target, mismatched name) before a human reviews the candidate.

  • Warn-only by default; ValidationContext.strictGeneratedContract escalates to
    block. All-declarative proposals (no generatedSource) stay "skipped".
  • Wired into validateProposal (was the M1 skip stub); exported from
    @linchkit/core/server (+ barrel-surface test).
  • action is the only materializable target today (matches the materializer's
    MATERIALIZABLE_TARGETS); the map is extensible.

Safety

EXECUTION-FREE by design ("AI never modifies production directly"): it never
evals, imports, transpiles-and-runs, or otherwise executes the generated
source — only static string/structural heuristics. A true execution-based
dry-run (running a generated handler against sample/historical data) requires a
locked-down sandbox to run untrusted AI code and is intentionally out of
scope
here — deferred to a separate, sandbox-gated step.

Review

3 rounds of codex review converged to clean. Hardenings applied along the way:

  • Tokens appearing only in a comment (// defineAction() or string literal no
    longer satisfy the call/import checks (strip comments, and strings for the
    call check; match real import/call structure via anchored regexes).
  • The name check uses a word-boundary match so a different name that merely
    CONTAINS the declared one (do_thing_v2 for do_thing) no longer passes.

Verification

  • bun run check / bun run typecheck — clean
  • bun run test — PASS (8 Phase 4 unit tests incl. comment/string and
    substring-name hardening; barrel surface test; packages/core 4170 tests green)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added Phase 4 validation to verify AI-generated source code properly imports from the core library and references declared definitions by name.
    • Validation is warn-only by default but can be enforced strictly via configuration.
  • Tests

    • Added comprehensive test suite for generated source code contract validation.

laofahai and others added 3 commits June 7, 2026 22:05
Replace the skipped Phase 4 stub with validatePhase4: a static, EXECUTION-FREE
check that any AI-materialized generatedSource actually defines the change's
declared target/name — the right define*() call, references its name, imports
@linchkit/core. Catches AI errors that pass the Phase 2 syntax gate yet are wrong
(empty scaffold, wrong target, mismatched name) before human review.

- Warn-only by default; ValidationContext.strictGeneratedContract escalates to
  block. All-declarative proposals (no generatedSource) stay 'skipped'.
- SAFETY: never eval/import/transpile-and-run the generated source. A true
  execution-based dry-run (sandboxed handler run against sample data) is
  intentionally out of scope — it would require a locked-down sandbox to run
  untrusted AI code; deferred to a separate sandbox-gated step.
- Wired into validateProposal; exported from @linchkit/core/server; barrel
  surface test + 6 unit tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address codex review: raw includes() could be satisfied by a token appearing
only in a comment (// defineAction() or a string literal, letting strict mode
false-pass a contract-invalid action. Now strip comments (and, for the call
check, string literals) and match real import/call structure via anchored
regexes. + test that comment/string-only mentions do not satisfy the contract.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address codex review: substring includes(name) let a different name that merely
contains the declared one (e.g. do_thing_v2 for do_thing) satisfy the check.
Match on a word boundary (\bname\b) so a contained-but-different name no longer
passes — snake_case _ is a word char, so do_thing won't match inside do_thing_v2.
+ test. Name escaped for safe RegExp interpolation.

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

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@laofahai, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 15 minutes and 22 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9f5abbde-642c-4c2b-89c7-93070acbb86a

📥 Commits

Reviewing files that changed from the base of the PR and between bd03122 and 4b2d364.

📒 Files selected for processing (2)
  • packages/core/src/__tests__/engine/validation-phase4.test.ts
  • packages/core/src/engine/validation-phase4.ts
📝 Walkthrough

Walkthrough

This PR introduces Phase 4 validation, a static contract check for AI-materialized generatedSource that verifies generated code defines the declared target, references the declared name as a whole word, and imports from @linchkit/core. The check is warn-only by default and can be enforced via ValidationContext.strictGeneratedContract. It is wired into validateProposal for all-declarative proposals.

Changes

Phase 4 Generated-Source Contract Validation

Layer / File(s) Summary
Phase 4 validation contract and implementation
packages/core/src/engine/validation-phase4.ts
Phase 4 performs three static checks on non-empty generatedSource: presence of the expected define* call, whole-word reference to declared name (comments stripped, strings preserved), and actual import/require from @linchkit/core. Returns skipped when no generated source is present; otherwise collects findings and gates them as warnings or errors based on strictGeneratedContract.
Validation engine wiring
packages/core/src/engine/validation-engine.ts
Extends ValidationContext with optional strictGeneratedContract flag and replaces the Phase 4 "skipped" placeholder in validateProposal with an actual validatePhase4() call, passing through the context's strictness setting.
Phase 4 contract test suite
packages/core/src/__tests__/engine/validation-phase4.test.ts
Comprehensive tests covering skip behavior (no/empty generatedSource), passing validation (well-formed defineAction with core import), warn-only default, missing name reference and missing import findings, comment/string hardening (mentions not satisfied by comments/strings), name word-boundary matching (substring containment fails), and strict mode escalation to errors.
Public exports and documentation
packages/core/src/exports/server/engines.ts, packages/core/__tests__/barrel-public-surface.test.ts, .changeset/g5-phase4-contract.md
Server barrel exports updated to include ValidatePhase4Options and validatePhase4; public-surface test snapshot includes new export; changeset documents the feature as warn-only by default with optional strictGeneratedContract enforcement.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • laofahai/linchkit#487: Wires validatePhase3 into the same validation-engine integration pattern, extending ValidationContext with strictCompatibility.
  • laofahai/linchkit#495: Adds Phase 2 validatePhase2 to the validation flow with generatedSource checks, using the same engine wiring approach.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and specifically describes the main change: implementing validation Phase 4 for generated-source contract checking.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering what the change does, safety considerations, and verification results, though it partially follows the template structure.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/g5-phase4-contract

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces Phase 4 validation, which statically verifies that AI-materialized generated source code conforms to its declared contract (e.g., calling the correct definition helper, referencing its declared name, and importing @linchkit/core). Feedback on the implementation highlights a parsing bug where URLs or slashes inside string literals are incorrectly stripped as comments, which breaks validation. Additionally, the reviewer recommends escaping the dot in the core import regex, pre-compiling regular expressions at the module level to improve performance, and adding a test case to prevent regressions.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread packages/core/src/engine/validation-phase4.ts Outdated
Comment thread packages/core/src/engine/validation-phase4.ts
Comment thread packages/core/src/engine/validation-phase4.ts Outdated
Comment thread packages/core/src/__tests__/engine/validation-phase4.test.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/core/src/engine/validation-engine.ts (1)

1-717: Split packages/core/src/engine/validation-engine.ts to satisfy the 500-line guideline

  • packages/core/src/engine/validation-engine.ts is ~717 lines (> 500).
  • Searches for existing tracking issues mentioning “validation-engine” / “validation engine” / “validation-engine.ts” didn’t surface one for splitting this file; consider creating an issue to break it into smaller per-phase/per-validator modules.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/engine/validation-engine.ts` around lines 1 - 717, The file
validation-engine.ts exceeds the 500-line guideline; split it into smaller
modules by extracting Phase 1 runner and each validator into separate files and
re-exporting a compact API: create a file for the phase runner (export
validatePhase1 and validateProposal), a module for helpers/state resolution
(resolveStateMachine, ValidationContext utilities), and separate validator
modules for validateEntity, validateAction, validateRule, validateStateDef,
validateEventDef, and validateViewDef; update imports/exports so the original
symbols (validatePhase1, validateProposal, validateEntity, validateAction,
validateRule, validateStateDef, validateEventDef, validateViewDef,
resolveStateMachine, ValidationContext) remain available from the engine
package, add a short tracking issue referencing “validation-engine.ts” for the
refactor, and ensure tests/builds still pass after replacing local references
with the new module paths.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/core/src/engine/validation-engine.ts`:
- Around line 1-717: The file validation-engine.ts exceeds the 500-line
guideline; split it into smaller modules by extracting Phase 1 runner and each
validator into separate files and re-exporting a compact API: create a file for
the phase runner (export validatePhase1 and validateProposal), a module for
helpers/state resolution (resolveStateMachine, ValidationContext utilities), and
separate validator modules for validateEntity, validateAction, validateRule,
validateStateDef, validateEventDef, and validateViewDef; update imports/exports
so the original symbols (validatePhase1, validateProposal, validateEntity,
validateAction, validateRule, validateStateDef, validateEventDef,
validateViewDef, resolveStateMachine, ValidationContext) remain available from
the engine package, add a short tracking issue referencing
“validation-engine.ts” for the refactor, and ensure tests/builds still pass
after replacing local references with the new module paths.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9a02bee2-1ad2-4024-b413-987b9c237835

📥 Commits

Reviewing files that changed from the base of the PR and between e9795d0 and bd03122.

📒 Files selected for processing (6)
  • .changeset/g5-phase4-contract.md
  • packages/core/__tests__/barrel-public-surface.test.ts
  • packages/core/src/__tests__/engine/validation-phase4.test.ts
  • packages/core/src/engine/validation-engine.ts
  • packages/core/src/engine/validation-phase4.ts
  • packages/core/src/exports/server/engines.ts

laofahai and others added 9 commits June 7, 2026 22:26
…se 4)

Address gemini review on #497:
- stripComments/stripCommentsAndStrings now walk the source ONCE, string- and
  comment-aware, so a comment marker inside a string literal (a URL's //, or
  /* */ split across two strings) is never treated as a comment. The old regex
  stripper could greedily swallow real code between a /* and */ that each live
  inside separate string literals. + regression test.
- Pre-compile the core-import / require detectors at module level instead of
  rebuilding them per change; fold the define-call detector into a per-target
  contract map. (@linchkit/core has no regex metachar, so the literals are exact.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address codex review: export { defineAction } from "@linchkit/core" re-exports
without creating a local binding, so a generated action that re-exports then
calls defineAction would fail. The core-import detector now matches import only
(not export-from). + test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address codex review: a string literal containing a full import statement could
satisfy the core-import check (it ran over the strings-kept view). Strippers are
now length-preserving, and the import check confirms each matched import/require
keyword sits in REAL code by cross-referencing the same index in the
strings-blanked view — a fake import inside a string is blanked there. + test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address codex review: the call/name checks were independent, so a source that
calls defineAction for a DIFFERENT action while mentioning the declared name
elsewhere (e.g. `const do_thing = 1; const other = defineAction({name:'other'})`)
passed. The check now requires the declared name be BOUND to the define call —
either `const <name> = defineAction(` (identifier, robust in blanked code) or a
real `defineAction({ … name: '<name>' … })` call (keyword confirmed in real code
via index cross-reference). Replaces the two independent call/name findings with
one tied finding. + tests.

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

Address codex review (two gaps):
- The import check now requires the SPECIFIC helper (e.g. defineAction) in the
  import clause — `import { defineEntity } from core` no longer satisfies a
  defineAction contract.
- The tied name match is bounded to the call's own options object (`[^}]*?` can't
  cross the first `}`), so a `name: "<declared>"` in unrelated later code no
  longer satisfies the contract.
+ tests for both. These remain best-effort STATIC heuristics (no execution, no
full parse); Phase 2 syntax + the graduation PR's CI build + human review are the
authoritative gates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address codex review: the registered ActionDefinition.name is the defineAction
options-object name: literal, not the variable it is assigned to. Dropped the
boundByConst shortcut (const do_thing = defineAction({ name: 'other' }) registers
'other', not 'do_thing'); the contract is now satisfied ONLY by a real
defineAction call whose own options name the declared action. + test. Simplified
CONTRACT_BY_TARGET to a target→helper-name map.

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

Address codex review: the require() branch accepted any require("@linchkit/core")
without checking the called helper was bound. Generated definition source is ESM
(the materializer prompts for import … from "@linchkit/core"), so the CommonJS
fallback is unnecessary surface — removed it. The import check now requires the
specific helper in a real ESM import clause; a require-based file gets an advisory
import warning (warn-only by default).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address codex review: import type { defineAction } is erased at runtime and
{ defineAction as da } binds da (not defineAction), so neither provides a usable
helper value. The import detector now excludes a leading import type and an
aliased-away helper. Deeper shapes (inline { type x }, namespace import) remain
advisory-only — the graduation PR's CI typecheck is authoritative. + tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@laofahai laofahai merged commit 745debd into main Jun 8, 2026
6 checks passed
@laofahai laofahai deleted the feat/g5-phase4-contract branch June 8, 2026 01:34
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.

1 participant