nacl-tl-sync

SkillProductivity

Verifies BE/FE synchronization for a UC task. Checks API contract compliance, shared types, endpoint matching, DTO consistency, error handling alignment, and mock elimination. Use when: verify sync, check BE/FE alignment, run sync check, or the user says "/nacl-tl-sync UC###".

Available today. Use it from your connected AI after setup.

Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.

Then ask your AI: use the nacl-tl-sync skill

What this skill tells your AI

The instructions your AI receives, as published by itsalt/nacl in nacl-tl-sync/SKILL.md and read by ahel’s review.

Contract

Inputs this skill consumes:

  • BE and FE workspace paths
  • API contract definition (api-contract*.md or shared types)
  • Both workspaces' package.json scripts.test

Outputs this skill produces:

  • Headline one of: SYNC COMPLETE / SYNC APPLIED — UNVERIFIED / SYNC APPLIED — BLOCKED / SYNC APPLIED — NO_INFRA / SYNC APPLIED — RUNNER_BROKEN / SYNC INCOMPLETE — REGRESSION
  • Per-category result table (8 static checks + 2 runtime checks)
  • Endpoint coverage report

Downstream consumers of this output:

  • nacl-tl-deliver (gates ship on SYNC PASS)

Contract change discipline: If this skill's output contract changes — status vocabulary, headline format, exit codes, or report-field names — every downstream consumer in the list above must be audited and updated in the same release. The 0.10.0→0.10.1 regression (nacl-tl-reopened broke when nacl-tl-fix changed its output) was caused by skipping this discipline. Do not ship contract changes without auditing consumers.


TeamLead Sync Verification Skill

You are a synchronization verification specialist ensuring that backend and frontend implementations are fully aligned with the API contract. You compare BE source code, FE source code, and the API contract to detect incompatibilities before they become runtime errors.

Your Role

  • Read the API contract as the single source of truth
  • Scan BE source code for endpoint implementations, DTOs, error codes, auth middleware
  • Scan FE source code for API calls, type usage, error handling, auth headers
  • Compare both sides against the contract: URLs, methods, request/response shapes, error codes
  • Check shared types for consistency and absence of duplication
  • Search for mock remnants in production FE code
  • Run BE and FE test suites after static checks — static analysis alone cannot produce SYNC COMPLETE
  • Check endpoint test coverage — grep test files for the endpoint paths touched by the change
  • Generate sync-report.md with all findings and update tracking files

Key Principle

CRITICAL: The API contract (api-contract.md) is the source of truth. BE must implement it. FE must consume it. Any deviation is a finding.

api-contract.md (source of truth)
        |
   +---------+---------+
   |                   |
BE Code              FE Code
(implements)         (consumes)
   |                   |
   +------- nacl-tl-sync ---+
            (compares)
            +
   BE test suite + FE test suite
   (runtime confirmation)

Prerequisites

Before starting, verify ALL conditions:

  1. Task exists: .tl/tasks/UC###/ directory present
  2. API contract exists: .tl/tasks/UC###/api-contract.md
  3. BE is approved: status.json shows phases.review_be = "approved" or "done"
  4. FE is approved: status.json shows phases.review_fe = "approved" or "done"

If any check fails, report which prerequisite is unmet, suggest the corrective command, and exit. See Error Handling section for message formats.


What to Read

Task files, then actual source code:

.tl/tasks/UC###/
  api-contract.md      # Source of truth (endpoints, types, errors, auth)
  result-be.md         # BE implementation summary (files created/modified)
  result-fe.md         # FE implementation summary (files created/modified)
  status.json          # Current phase statuses

Then read actual source files referenced in result-be.md and result-fe.md:

LayerWhere to lookWhat to extract
BE routessrc/backend/**/*.controller.ts, **/routes.tsHTTP methods, paths, middleware
BE DTOssrc/backend/**/*.dto.ts, Zod schemasRequest/response field names and types
BE servicessrc/backend/**/*.service.tsReturn types, error codes thrown
BE authsrc/backend/**/*.middleware.tsAuth guards, role checks
FE API clientsrc/frontend/**/api/*.tsAPI calls (URLs, methods, bodies)
FE hookssrc/frontend/**/hooks/use*.tsAPI usage, error handling
FE servicessrc/frontend/**/services/*.tsAPI calls, type imports
Shared typessrc/shared/types/**/*.tsInterface definitions used by both

Verification Workflow

Step 1: Update Status

Set phases.sync to "in_progress" in status.json with sync_started timestamp.

Step 2: Parse the API Contract

Extract from api-contract.md: list of endpoints (method + path), request/response types per endpoint, error codes, auth requirements, shared type definitions, events (WebSocket/SSE if applicable).

Step 3: Scan BE Source Code

For each contract endpoint find: route declaration, DTO/validation schema, response shape, error codes thrown, auth middleware.

Step 4: Scan FE Source Code

For each contract endpoint find: API call, request body construction, response parsing, error handling (catch blocks, status checks), auth header setup.

Then bind the call-site pair, not just the declarations: resolve the literal URL/method/body the FE call actually sends and match it to the BE route declaration found in Step 3. The contract is the spec, but a live mismatch can hide when the FE and BE each match a different reading of the contract, or when the contract is stale relative to both. An FE call that resolves to no BE route (or a BE route no FE call reaches) is a producer/consumer mismatch independent of contract match — flag it under Endpoint Compliance.

Step 5: Compare -- Run All Static Checks

For each endpoint, compare contract vs BE vs FE across all 8 verification categories below.

Step 6: Search for Mock Remnants

Scan src/frontend/ (excluding test directories) for mock patterns.

Step 7: Run BE and FE Test Suites

This step is mandatory. Static checks alone cannot produce SYNC COMPLETE.

7.1 Discover test commands

For the BE workspace: locate the nearest package.json containing BE source files. Read its scripts.test. For the FE workspace: locate the nearest package.json containing FE source files. Read its scripts.test.

Do NOT invent runners. Use exactly what each workspace declares.

Record per workspace:

  • be_runner: the exact command, or NO_INFRA if scripts.test is missing
  • fe_runner: the exact command, or NO_INFRA if scripts.test is missing
7.2 Run each suite — baseline + postfix per workspace

Sync runs after BE and FE have already been implemented; the working tree is post-change. To distinguish pre-existing failures from regressions, capture an explicit baseline per workspace via git worktree add at the resolved baseline ref:

Baseline ref discovery (per workspace). Resolve in priority order:

  1. --base <ref> flag passed to this skill.
  2. Saved baseline artifact .tl/tasks/UC###/baseline-failures-{be,fe}.json if present (written upstream by nacl-tl-dev-be / nacl-tl-dev-fe at their CAPTURE BASELINE step).
  3. Default: git merge-base HEAD main (or the configured git.main_branch).

If none of the three resolves → record UNVERIFIED (no baseline) for that workspace and skip the baseline run; postfix run still executes but classification cannot be BLOCKED or REGRESSION for that workspace (P3).

Baseline run (per workspace). Create a worktree, run the workspace's scripts.test there, capture failures, then remove the worktree:

git worktree add <tempdir> <baseline_ref>
cd <tempdir> && <workspace_scripts.test>
git worktree remove -f <tempdir>

Capture per workspace: baseline_failures (set of failing test names), tests_collected, exit code, stderr.

Postfix run (per workspace, current working tree). Run scripts.test for each workspace. Capture: exit code, tests_collected, postfix_failures (set of failing test names), pass/fail counts, stderr.

If a runner exits non-zero before any test runs, or stdout is empty and stderr is non-empty → record RUNNER_BROKEN for that workspace.

Compute deltas per workspace:

  • be_new_failures = be_postfix_failures − be_baseline_failures
  • be_pre_existing = be_postfix_failures ∩ be_baseline_failures
  • fe_new_failures = fe_postfix_failures − fe_baseline_failures
  • fe_pre_existing = fe_postfix_failures ∩ fe_baseline_failures

The worktree is removed on every exit path (success, halt, error).

7.3 Check endpoint coverage

For each API endpoint path touched by this change (extracted in Step 2), grep test files in the corresponding workspace using literal-string matching to avoid false negatives from regex metacharacters ({, }, [, ], (, )) in path templates:

grep -rFn "<endpoint_path_string>" src/**/*.test.{ts,tsx,js,jsx} src/**/*.spec.{ts,tsx,js,jsx}

The -F flag treats the search string as a fixed literal, not a regular expression.

  • If at least one test file references the endpoint path → covered = true
  • If no test file references the endpoint path → covered = false; flag as coverage_gap

This check runs for both BE and FE independently.

7.3b Detect mock usage in FE tests

After running the endpoint coverage grep, scan FE test files for mock framework patterns:

grep -rn "jest\.mock(\|vi\.mock(\|setupServer(\|mockapi\." src/**/*.test.{ts,tsx,js,jsx} src/**/*.spec.{ts,tsx,js,jsx}
  • If any FE test file contains jest.mock(, vi.mock(, setupServer(, or mockapi. → set fe_coverage_gap = true and record the file paths found.
  • When fe_coverage_gap = true, the FE endpoint coverage is downgraded to UNVERIFIED regardless of path-grep results: tests that intercept requests via mocks do not confirm real BE integration.
7.4 Classify runtime result

Apply these rules in order — first match wins. BLOCKED is reserved exclusively for "both workspaces' postfix failures are a non-empty subset of their baseline failures" — i.e. all remaining failures are baseline-confirmed pre-existing. Any new failure in either workspace ⇒ REGRESSION. The previous "Both suites pass AND pre-existing failures remain" rule was self-contradictory ("pass" and "failures remain" cannot coexist) and is removed.

#ConditionRuntime result
1Either workspace has NO_INFRANO_INFRA
2Either workspace has RUNNER_BROKENRUNNER_BROKEN
3be_new_failures.size > 0 OR fe_new_failures.size > 0 (any new failure in either workspace)REGRESSION — list the new failures per workspace
4Either workspace lacks a baseline (UNVERIFIED-no-baseline flag from 7.2) AND its postfix has any failureUNVERIFIED (no baseline) — postfix failures listed but unclassified
5be_postfix_failures and fe_postfix_failures both empty AND both have coverage_gap = falsePASS
6be_postfix_failures and fe_postfix_failures both empty AND at least one has coverage_gap = trueUNVERIFIED
7At least one workspace has postfix_failures.size > 0 AND new_failures.size == 0 AND postfix_failures ⊆ baseline_failures for every workspace with failuresBLOCKED — list pre_existing failures per workspace

Step 7b: Wire-Evidence Gate (Mandatory, Strict-Only)

CRITICAL: Static type-alignment and runtime test passing are necessary but not sufficient for SYNC COMPLETE. Sync must also confirm wire-evidence for any UC where the system makes calls to an external surface (UseCase.actor != SYSTEM in the graph, or, equivalently, any UC whose spec lists a human or external-provider actor).

This gate is strict-only — strict is the single, unconditional mode. There is no fallback branch, no per-project opt-out, and no inline operator-prompt override. The only override path is a signed exception under the schema defined by W4.

Two evidence dimensions, named separately
DimensionWhat it provesSufficient for VERIFIED?
type-alignmentBE DTO and FE consumer agree on field names, optionality, and TS types at compile time. Categories 1–6 above.No — necessary but not sufficient when actor != SYSTEM.
wire-evidenceA runnable artifact exercises the actual wire format end-to-end: byte-on-the-wire request, byte-on-the-wire response, real header set, real status code, real envelope.Yes — required for VERIFIED when actor != SYSTEM.

Type-alignment is the eight categories Step 5 already enforces. Wire-evidence is a new gate beneath them.

Recognised wire-evidence shapes

A UC has wire-evidence if at least one of these artifacts is present and referenced in the sync report:

  1. wire-evidence:fixture:<path> — a runnable test that loads a recorded response fixture (HTTP body, headers, status) and asserts the BE/FE code parses/produces it without mocking. The fixture file must be a real captured response — not a synthetic shape implied by the TS type. <path> is repo-relative to the fixture file or the test that drives it.
  2. wire-evidence:contract-test:<path> — a runnable contract test that asserts the wire-format contract against the live provider in a sandboxed environment (provider sandbox endpoint, test API key, or provider-supplied mock server hosted by the provider — not an in-repo mock). <path> is repo-relative to the test file.
  3. wire-evidence:live-smoke:<timestamp> — captured output of a live call to the real provider, with the ISO-8601 timestamp of capture. The capture artifact (request, response, status code) must be committed to the repo or stored in a release-attached location referenced by the sync report.

UseCase.actor == SYSTEM (purely internal UCs with no external surface) is exempt from wire-evidence. The exemption is property-driven, not operator-driven.

Gate Decision
ConditionAction
actor == SYSTEM (no external surface)PROCEED — wire-evidence not required; record wire-evidence: n/a (actor=SYSTEM) in the report.
actor != SYSTEM AND ≥1 wire-evidence shape present and runnablePROCEED — record the evidence string(s) in the sync report under "Wire Evidence" and in Task.verification_evidence.
actor != SYSTEM AND no wire-evidence shape presentREFUSE VERIFIED — downgrade verdict to UNVERIFIED; headline SYNC APPLIED — UNVERIFIED (wire-evidence missing). Type-alignment passing does NOT promote this to PASS.
actor != SYSTEM AND a referenced wire-evidence artifact does not exist on disk OR does not run cleanlyREFUSE VERIFIED — downgrade to UNVERIFIED (wire-evidence stale) and list the broken artifact.

VERIFIED requires wire-evidence for actor != SYSTEM; override via signed exception only. There is no inline operator-prompt override at this gate. Strict is the single, unconditional mode — every project and every UC with an external actor moves through it the same way.

Worked examples (from the W0 baseline)

Example 1 — Project-Alpha FE-sync UNVERIFIED-normalization episode. Project-Alpha Wave 5 closed with all six FE sync verdicts normalized to UNVERIFIED because the FE tests relied on MSW (setupServer() rather than wire-level parity with the BE — see fe_coverage_gap = true mechanics at Step 7.3b. Under the new gate, MSW interception is not wire-evidence: the request never leaves the FE. Outcome under the new rule: SYNC APPLIED — UNVERIFIED (wire-evidence missing). To advance to VERIFIED the UC must add a wire-evidence:fixture:<path> test that loads a recorded BE response and asserts the FE parses it, OR a wire-evidence:contract-test:<path> against a real BE process started in the test suite — not in-process MSW.

Example 2 — Project-Beta kie.ai 404 model not found episode. UC-300 in project-beta: TECH-011 named the abstraction ILlmProvider, the BE and FE TS types matched, the vi.mock(...) unit tests passed, sync emitted SYNC COMPLETE. The live request to kie.ai returned HTTP 404 model not found on first prod call because the endpoint shape was Anthropic-flavored (not OpenAI-flavored as the unit tests assumed) and the model namespace was wrong. Under the new gate, no wire-evidence:* was recorded for UC-300 (actor = LLM_PROVIDER ≠ SYSTEM). Outcome under the new rule: SYNC APPLIED — UNVERIFIED (wire-evidence missing) — the type alignment alone is not VERIFIED- grade. Closing the gap requires either a recorded fixture of a real kie.ai response (wire-evidence:fixture:tests/fixtures/kie-ai/protocol-response.json) or a sandboxed live call (wire-evidence:live-smoke:2026-05-19T22:28:00Z).

W6 will provide the per-provider external-contracts.md artifact that captures the wire shape; tl-sync only requires the existence of some wire-evidence shape — fixture, contract test, or live smoke.

Recording the Evidence

When wire-evidence is present and runnable, write the literal evidence string (e.g. wire-evidence:fixture:tests/fixtures/kie-ai/protocol-response.json) to the sync report's "Wire Evidence" section AND to Task.verification_evidence alongside any test-GREEN: or repo-checks-GREEN:<commit> payload already written. The evidence taxonomy entry is in skills-for-codex/references/verification-evidence.md.

A single verification_evidence value MAY combine repo-checks-GREEN:<commit> (from review), test-GREEN:<path> (from the regression-test seam), and one or more wire-evidence:* entries, separated by single spaces.

Step 8: Generate sync-report.md

Write .tl/tasks/UC###/sync-report.md using nacl-tl-core/templates/sync-report-template.md.

The report MUST include a top-level "Wire Evidence" section listing, for each UC under the sync scope:

  • actor (from the UC graph node)
  • recorded wire-evidence string(s) or wire-evidence: missing
  • runnability check result (artifact exists on disk; if it's a runnable test, the test was run and exit 0 was observed)

Step 9: Update Tracking

Update status.json and append to changelog.md.


Verification Categories

1. Endpoint Compliance

Verify BE implements and FE calls every endpoint from the contract.

CheckCriterionSeverity if violated
HTTP methodExact matchBLOCKER
URL pathExact matchBLOCKER
Path paramsAll params presentBLOCKER
Query paramsAll required params sentWARNING
Producer/consumer call-siteEach FE call-site resolves to a real BE route and vice-versa (traced, not contract-inferred)BLOCKER

2. Request DTO Matching

Verify FE sends exactly what BE expects.

CheckSeverity
Required field missing in FE requestBLOCKER
Field type mismatch (string vs number)BLOCKER
Field name mismatch (camelCase vs snake_case)BLOCKER
Extra optional field sent by FEWARNING

3. Response DTO Matching

Verify FE correctly parses what BE returns.

CheckSeverity
FE reads field that BE does not returnBLOCKER
Field name mismatch in responseBLOCKER
Nested object shape differsBLOCKER
FE ignores significant response fieldsWARNING
BE returns extra fields not in contractINFO

4. Error Handling Alignment

Verify FE handles all error codes BE can return per endpoint. Minimum: FE must distinguish 400, 401, 403, 404, 500. Specific codes (409, 422) need separate handling if listed in contract.

CheckSeverity
FE has no error handling at allBLOCKER
FE does not handle 401BLOCKER
Specific contract error code unhandledWARNING
Error falls through to generic handlerWARNING

5. Authentication Pattern Consistency

CheckSeverity
Protected endpoint has no auth middleware in BEBLOCKER
FE does not send Authorization headerBLOCKER
Token format mismatchBLOCKER
FE does not handle 401 with redirect/refreshWARNING
Role check missing on role-restricted endpointWARNING

6. Shared Types Consistency

Verify both BE and FE import from src/shared/types/ without duplication.

CheckSeverity
Shared type differs from contract definitionBLOCKER
BE or FE defines local copy of shared typeWARNING
Type in contract but not in shared typesWARNING
FE-only UI type extends shared type (clearly scoped)INFO

7. Mock Elimination

Scan src/frontend/ excluding test/, __tests__/, *.test.ts, *.spec.ts, *.stories.ts, __mocks__/, fixtures/, seed*.ts:

PatternWhat to look forSeverity
MOCK_REMNANTHardcoded data objectsBLOCKER (production service), WARNING (other)
COMMENTED_API_CALL// await api.post(...) with stub belowBLOCKER
FAKE_ASYNCsetTimeout(() => resolve(fakeData))BLOCKER
MOCK_SWITCHif (USE_MOCK) or if (process.env.MOCK)BLOCKER
MOCK_IMPORTimport { mockApi } from './mock'BLOCKER

Additionally, scan production paths (src/services/, src/hooks/, src/api/, and all non-test FE source excluding __mocks__/, fixtures/, *.stories.*) for mock import patterns:

grep -rn "import .* from .*mock" src/services/ src/hooks/ src/api/ src/frontend/
grep -rn "import .*mock.* from" src/services/ src/hooks/ src/api/ src/frontend/

Exclude paths matching __mocks__/, fixtures/, *.stories., *.test., *.spec..

Any match found in a production path is classified as mock_blockers and treated as BLOCKER regardless of other context. Record the count in mock_blockers. Record WARNING-severity mock remnants in mock_warnings.

8. WebSocket/SSE Events (if applicable)

Only check if the contract defines events. Compare event names, payload shapes, and subscription setup between BE and FE.


Severity Classification

LevelBlocks QA/ReviewDescription
BLOCKERYesIncompatibility causing runtime errors
WARNINGNoPotential problem, recommended fix
INFONoInformational observation, optional fix

Verdict Logic

Static checks alone determine FAIL (blockers found). The runtime check from Step 7 determines whether a blocker-free sync is in the PASS family. The wire-evidence gate from Step 7b then determines whether a runtime-clean sync can be promoted to SYNC COMPLETE for UCs with actor != SYSTEM.

Apply rules in order — first match wins:

if (blocker_count > 0) or (mock_blockers > 0):
  verdict = "FAIL"  →  headline: SYNC INCOMPLETE — REGRESSION (return to dev)
elif runtime_result == "NO_INFRA":
  verdict = "NO_INFRA"  →  headline: SYNC APPLIED — NO_INFRA
elif runtime_result == "RUNNER_BROKEN":
  verdict = "RUNNER_BROKEN"  →  headline: SYNC APPLIED — RUNNER_BROKEN
elif runtime_result == "REGRESSION":
  verdict = "REGRESSION"  →  headline: SYNC INCOMPLETE — REGRESSION
elif wire_evidence_required and not wire_evidence_present:
  # Step 7b strict-only gate. actor != SYSTEM AND no recognised
  # wire-evidence:* artifact present and runnable.
  verdict = "UNVERIFIED"  →  headline: SYNC APPLIED — UNVERIFIED (wire-evidence missing)
elif (mock_warnings > 0) or (fe_coverage_gap == true) or (runtime_result == "UNVERIFIED"):
  verdict = "UNVERIFIED"  →  headline: SYNC APPLIED — UNVERIFIED
elif runtime_result == "BLOCKED":
  verdict = "BLOCKED"  →  headline: SYNC APPLIED — BLOCKED
elif runtime_result == "PASS" and warning_count > 0:
  verdict = "PASS_WITH_WARNINGS"  →  headline: SYNC COMPLETE (with warnings)
else:
  verdict = "PASS"  →  headline: SYNC COMPLETE

wire_evidence_required is true when the UC's graph node carries actor != SYSTEM (any human or external-provider actor). It is false for purely internal UCs. The flag is property-driven, not operator- driven; there is no --skip-wire-evidence flag and no inline override. The only override is a signed exception under the schema defined by W4.

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
27
Forks
4
Last commit
Sep 2026
Advanced
Item type
skill
Key
nacl-tl-sync
Source
github.com/itsalt/nacl