nacl-tl-sync
SkillProductivityVerifies 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.
No other account needed.
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.jsonscripts.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:
- Task exists:
.tl/tasks/UC###/directory present - API contract exists:
.tl/tasks/UC###/api-contract.md - BE is approved:
status.jsonshowsphases.review_be="approved"or"done" - FE is approved:
status.jsonshowsphases.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:
| Layer | Where to look | What to extract |
|---|---|---|
| BE routes | src/backend/**/*.controller.ts, **/routes.ts | HTTP methods, paths, middleware |
| BE DTOs | src/backend/**/*.dto.ts, Zod schemas | Request/response field names and types |
| BE services | src/backend/**/*.service.ts | Return types, error codes thrown |
| BE auth | src/backend/**/*.middleware.ts | Auth guards, role checks |
| FE API client | src/frontend/**/api/*.ts | API calls (URLs, methods, bodies) |
| FE hooks | src/frontend/**/hooks/use*.ts | API usage, error handling |
| FE services | src/frontend/**/services/*.ts | API calls, type imports |
| Shared types | src/shared/types/**/*.ts | Interface 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, orNO_INFRAifscripts.testis missingfe_runner: the exact command, orNO_INFRAifscripts.testis 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:
--base <ref>flag passed to this skill.- Saved baseline artifact
.tl/tasks/UC###/baseline-failures-{be,fe}.jsonif present (written upstream bynacl-tl-dev-be/nacl-tl-dev-feat their CAPTURE BASELINE step). - Default:
git merge-base HEAD main(or the configuredgit.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_failuresbe_pre_existing = be_postfix_failures ∩ be_baseline_failuresfe_new_failures = fe_postfix_failures − fe_baseline_failuresfe_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 ascoverage_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(, ormockapi.→ setfe_coverage_gap = trueand 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.
| # | Condition | Runtime result |
|---|---|---|
| 1 | Either workspace has NO_INFRA | NO_INFRA |
| 2 | Either workspace has RUNNER_BROKEN | RUNNER_BROKEN |
| 3 | be_new_failures.size > 0 OR fe_new_failures.size > 0 (any new failure in either workspace) | REGRESSION — list the new failures per workspace |
| 4 | Either workspace lacks a baseline (UNVERIFIED-no-baseline flag from 7.2) AND its postfix has any failure | UNVERIFIED (no baseline) — postfix failures listed but unclassified |
| 5 | be_postfix_failures and fe_postfix_failures both empty AND both have coverage_gap = false | PASS |
| 6 | be_postfix_failures and fe_postfix_failures both empty AND at least one has coverage_gap = true | UNVERIFIED |
| 7 | At least one workspace has postfix_failures.size > 0 AND new_failures.size == 0 AND postfix_failures ⊆ baseline_failures for every workspace with failures | BLOCKED — 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
| Dimension | What it proves | Sufficient for VERIFIED? |
|---|---|---|
| type-alignment | BE 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-evidence | A 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:
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.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.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
| Condition | Action |
|---|---|
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 runnable | PROCEED — record the evidence string(s) in the sync report under "Wire Evidence" and in Task.verification_evidence. |
actor != SYSTEM AND no wire-evidence shape present | REFUSE 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 cleanly | REFUSE 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.
| Check | Criterion | Severity if violated |
|---|---|---|
| HTTP method | Exact match | BLOCKER |
| URL path | Exact match | BLOCKER |
| Path params | All params present | BLOCKER |
| Query params | All required params sent | WARNING |
| Producer/consumer call-site | Each 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.
| Check | Severity |
|---|---|
| Required field missing in FE request | BLOCKER |
| Field type mismatch (string vs number) | BLOCKER |
| Field name mismatch (camelCase vs snake_case) | BLOCKER |
| Extra optional field sent by FE | WARNING |
3. Response DTO Matching
Verify FE correctly parses what BE returns.
| Check | Severity |
|---|---|
| FE reads field that BE does not return | BLOCKER |
| Field name mismatch in response | BLOCKER |
| Nested object shape differs | BLOCKER |
| FE ignores significant response fields | WARNING |
| BE returns extra fields not in contract | INFO |
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.
| Check | Severity |
|---|---|
| FE has no error handling at all | BLOCKER |
| FE does not handle 401 | BLOCKER |
| Specific contract error code unhandled | WARNING |
| Error falls through to generic handler | WARNING |
5. Authentication Pattern Consistency
| Check | Severity |
|---|---|
| Protected endpoint has no auth middleware in BE | BLOCKER |
| FE does not send Authorization header | BLOCKER |
| Token format mismatch | BLOCKER |
| FE does not handle 401 with redirect/refresh | WARNING |
| Role check missing on role-restricted endpoint | WARNING |
6. Shared Types Consistency
Verify both BE and FE import from src/shared/types/ without duplication.
| Check | Severity |
|---|---|
| Shared type differs from contract definition | BLOCKER |
| BE or FE defines local copy of shared type | WARNING |
| Type in contract but not in shared types | WARNING |
| 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:
| Pattern | What to look for | Severity |
|---|---|---|
| MOCK_REMNANT | Hardcoded data objects | BLOCKER (production service), WARNING (other) |
| COMMENTED_API_CALL | // await api.post(...) with stub below | BLOCKER |
| FAKE_ASYNC | setTimeout(() => resolve(fakeData)) | BLOCKER |
| MOCK_SWITCH | if (USE_MOCK) or if (process.env.MOCK) | BLOCKER |
| MOCK_IMPORT | import { 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
| Level | Blocks QA/Review | Description |
|---|---|---|
| BLOCKER | Yes | Incompatibility causing runtime errors |
| WARNING | No | Potential problem, recommended fix |
| INFO | No | Informational 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